From da8defb3884ffa640924d7dbf6b0dc87c00b2a20 Mon Sep 17 00:00:00 2001 From: gesinn-it-gea Date: Wed, 19 Nov 2025 18:42:18 +0100 Subject: [PATCH 1/6] work in progress for #34, #35 --- extension.json | 3 +- i18n/de.json | 10 +- i18n/en.json | 2 + includes/KnowledgeGraph.php | 167 +- includes/api/KnowledgeGraphApiLoadNodes.php | 87 +- .../SpecialKnowledgeGraphDesigner.php | 3 +- resources/KnowledgeGraph.css | 46 +- resources/KnowledgeGraph.js | 1400 ++++++++--------- resources/KnowledgeGraphDialog.js | 64 +- resources/KnowledgeGraphFunctions.js | 2 +- tests/phpunit/Unit/KnowledgeGraphTest.php | 6 +- 11 files changed, 896 insertions(+), 894 deletions(-) diff --git a/extension.json b/extension.json index d683382..e933188 100644 --- a/extension.json +++ b/extension.json @@ -36,7 +36,6 @@ "KnowledgeGraphDesigner": "SpecialKnowledgeGraphDesigner" }, "Hooks":{ - "LoadExtensionSchemaUpdates": "KnowledgeGraph::onLoadExtensionSchemaUpdates", "BeforePageDisplay":"KnowledgeGraph::onBeforePageDisplay", "ParserFirstCallInit": "KnowledgeGraph::onParserFirstCallInit", "OutputPageParserOutput": "KnowledgeGraph::onOutputPageParserOutput", @@ -91,6 +90,8 @@ "knowledgegraph-dialog-cancel", "knowledgegraph-dialog-delete", "knowledgegraph-dialog-select-article", + "knowledgegraph-dialog-select-namespace", + "knowledgegraph-dialog-main-namespace", "knowledgegraph-dialog-edit-depth", "knowledgegraph-dialog-edit-limit", "knowledgegraph-dialog-edit-offset", diff --git a/i18n/de.json b/i18n/de.json index 810be16..58bc0ee 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -13,7 +13,7 @@ "knowledgegraph-knowledgegraphdesigner-label": "KnowledgeGraph Designer", "knowledgegraph-toolbar-info": "Info", "knowledgegraph-toolbar-help": "Hilfe", - "knowledgegraph-graph-options-message": "Klicken Sie auf die Schaltfläche unten und kopieren Sie den Inhalt der Tasten \"nodes\" und \"edges\" in einen Artikel wie diesen. Dann setzen Sie ihn als Wert der Parameter \"graph-options\" oder \"property-options?[Property label] der Parser-Funktion des KnowledgeGraphs.", + "knowledgegraph-graph-options-message": "{{#FORMAL:Klicke|Klicken Sie}} auf die Schaltfläche unten und {{#FORMAL:kopiere|kopieren Sie}} den Inhalt der Tasten \"nodes\" und \"edges\" in einen Artikel wie diesen. Dann {{#FORMAL:setze|setzen Sie}} ihn als Wert der Parameter \"graph-options\" oder \"property-options?[Property label] der Parser-Funktion des KnowledgeGraphs.", "knowledgegraph-menu-open-article": "Seite öffnen", "knowledgegraph-menu-delete-node": "Knoten entfernen", "knowledgegraph-delete-node-confirm": "Diesen Knoten sicher entfernen?", @@ -22,7 +22,9 @@ "knowledgegraph-dialog-done": "Fertig", "knowledgegraph-dialog-cancel": "Abbrechen", "knowledgegraph-dialog-delete": "Löschen", - "knowledgegraph-dialog-select-article": "Wählen Sie einen Artikel mit semantischen Attributen", + "knowledgegraph-dialog-select-article": "{{#FORMAL:Wähle|Wählen Sie}} einen Artikel mit semantischen Attributen", + "knowledgegraph-dialog-select-namespace": "{{#FORMAL:Wähle|Wählen Sie}} einen Namensraum", + "knowledgegraph-dialog-main-namespace": "(Main)", "knowledgegraph-dialog-edit-depth": "Tiefe", "knowledgegraph-dialog-edit-limit": "Limit", "knowledgegraph-dialog-edit-offset": "Versatz", @@ -42,10 +44,10 @@ "knowledgegraph-dialog-results-importing-nodes": "importiert Knoten:", "knowledgegraph-copied-to-clipboard": "In die Zwischenablage kopiert!", "knowledgegraph-toolbar-reset-network": "zurücksetzen", - "knowledgegraph-toolbar-reset-network-confirm": "Bist du sicher, dass du das Netzwerk neu initialisieren willst?", + "knowledgegraph-toolbar-reset-network-confirm": "{{#FORMAL:Bist du|Sind Sie}} sicher, dass {{#FORMAL:du|Sie}} das Netzwerk neu initialisieren {{#FORMAL:willst|wollen}}?", "knowledgegraph-toolbar-add-node": "Knoten hinzufügen", "knowledgegraph-toolbar-toggle-config": "Konfiguration umschalten", "knowledgegraph-toolbar-export-graph": "exportiere Wikitext", "knowledgegraph-credits": "Info:", - "knowledgegraph-credits-list": "" + "knowledgegraph-credits-list": "" } diff --git a/i18n/en.json b/i18n/en.json index 4d9e839..2063415 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -26,6 +26,8 @@ "knowledgegraph-dialog-cancel": "Cancel", "knowledgegraph-dialog-delete": "Delete", "knowledgegraph-dialog-select-article": "Select an article with semantic properties", + "knowledgegraph-dialog-select-namespace": "Select a namespace", + "knowledgegraph-dialog-main-namespace": "(Main)", "knowledgegraph-dialog-edit-depth": "Depth", "knowledgegraph-dialog-edit-limit": "Limit", "knowledgegraph-dialog-edit-offset": "Offset", diff --git a/includes/KnowledgeGraph.php b/includes/KnowledgeGraph.php index f8b56f3..4d58c87 100644 --- a/includes/KnowledgeGraph.php +++ b/includes/KnowledgeGraph.php @@ -134,38 +134,57 @@ public static function initSMW() { * @return void */ public static function onBeforePageDisplay( $out, $skin ) { - $out->addModules( 'ext.KnowledgeGraph' ); + // Ensure that the KnowledgeGraphOptions page exists + self::ensureKnowledgeGraphOptionsPageExists(); return true; } /** - * @param Parser $parser + * Ensure that the KnowledgeGraphOptions page exists in the MediaWiki namespace. + * Creates it lazily if missing. + * + * @return void */ - public static function onParserFirstCallInit( Parser $parser ) { - $parser->setFunctionHook( 'knowledgegraph', [ self::class, 'parserFunctionKnowledgeGraph' ] ); - } + private static function ensureKnowledgeGraphOptionsPageExists() { + $title = Title::makeTitleSafe( NS_MEDIAWIKI, 'KnowledgeGraphOptions' ); + if ( !$title ) { + return; + } - /** - * @param DatabaseUpdater|null $updater - */ - public static function onLoadExtensionSchemaUpdates( DatabaseUpdater $updater = null ) { - $text = file_get_contents( __DIR__ . '/../data/KnowledgeGraphOptions.js' ); - $user = RequestContext::getMain()->getUser(); - $title = TitleClass::makeTitleSafe( NS_MEDIAWIKI, 'KnowledgeGraphOptions' ); + $wikiPage = WikiPage::factory( $title ); + if ( $wikiPage->exists() ) { + return; + } - $wikiPage = self::getWikiPage( $title ); - $pageUpdater = $wikiPage->newPageUpdater( $user ); + // Create page content + $filePath = __DIR__ . '/../data/KnowledgeGraphOptions.js'; + if ( !file_exists( $filePath ) ) { + wfDebugLog( 'KnowledgeGraph', 'Missing KnowledgeGraphOptions.js template file.' ); + return; + } - // @see includes/Defines.php - $modelId = CONTENT_MODEL_JAVASCRIPT; - $slotContent = ContentHandler::makeContent( $text, $title, $modelId ); - $slotName = SlotRecord::MAIN; - $pageUpdater->setContent( $slotName, $slotContent ); + $text = file_get_contents( $filePath ); + $content = ContentHandler::makeContent( + $text, + $title, + CONTENT_MODEL_JAVASCRIPT + ); - $summary = "KnowledgeGraph"; - $flags = EDIT_INTERNAL; - $comment = CommentStoreComment::newUnsavedComment( $summary ); - $pageUpdater->saveRevision( $comment, $flags ); + $user = User::newSystemUser( 'MediaWiki default', [ 'steal' => true ] ); + + $pageUpdater = $wikiPage->newPageUpdater( $user ); + $pageUpdater->setContent( SlotRecord::MAIN, $content ); + $pageUpdater->saveRevision( + CommentStoreComment::newUnsavedComment( 'Initialize KnowledgeGraphOptions' ), + EDIT_SUPPRESS_RC + ); + } + + /** + * @param Parser $parser + */ + public static function onParserFirstCallInit( Parser $parser ) { + $parser->setFunctionHook( 'knowledgegraph', [ self::class, 'parserFunctionKnowledgeGraph' ] ); } /** @@ -277,6 +296,7 @@ public static function parserFunctionKnowledgeGraph( Parser $parser, ...$argv ) $params['graphOptions'] = $graphOptions; $params['propertyOptions'] = $propertyOptions; self::$graphs[] = $params; + self::$data = []; $out->setExtensionData( 'knowledgegraphs', self::$graphs ); @@ -290,8 +310,9 @@ public static function parserFunctionKnowledgeGraph( Parser $parser, ...$argv ) 'wgKnowledgeGraphColorPalette' => $colors ] ); + $index = count( self::$graphs ) - 1; return [ - '
' + '
' . wfMessage( 'knowledge-graph-wrapper-loading' )->text() . '
', 'noparse' => true, 'isHTML' => true @@ -358,6 +379,75 @@ public static function getSubjectsByProperty( $propertyText, $limit = 100, $offs return $ret; } + /** + * Get all properties for a given node. + * @param string $nodeTitleText + * @return array + */ + public static function getAllPropertiesForNode( string $nodeTitleText ): array { + $ret = []; + + $title = Title::newFromText( $nodeTitleText ); + if ( !$title || !$title->isKnown() ) { + wfDebugLog( 'KnowledgeGraph', "Invalid or unknown node: '$nodeTitleText'" ); + return []; + } + + $apiParams = [ + 'action' => 'smwbrowse', + 'format' => 'json', + 'browse' => 'subject', + 'params' => json_encode( [ + 'subject' => $nodeTitleText, + 'ns' => $title->getNamespace(), + ] ), + ]; + + $request = new \FauxRequest( $apiParams, false ); + $api = new \ApiMain( $request ); + $api->execute(); + $data = $api->getResult()->getResultData(); + + if ( empty( $data[ 'query' ][ 'data' ] ) ) { + wfDebugLog( 'KnowledgeGraph', "No properties returned from smwbrowse for '$nodeTitleText'" ); + return []; + } + + foreach ( $data['query']['data'] as $propertyEntry ) { + $propKey = $propertyEntry['property'] ?? null; + $direction = $propertyEntry['direction'] ?? 'direct'; + + if ( !$propKey ) { + continue; + } + + if ( + ( isset( self::$exclude ) && in_array( $propKey, self::$exclude ) ) || + str_starts_with( $propKey, '_' ) || + str_starts_with( $propKey, '___' ) || + ctype_upper( str_replace( '_', '', $propKey ) ) + ) { + continue; + } + + $propKey = str_replace( '_', ' ', $propKey ); + + if ( $direction === 'inverse' ) { + $propKey = '-' . $propKey; + } + + $ret[] = $propKey; + } + + wfDebugLog( 'KnowledgeGraph', sprintf( + "getAllPropertiesForNode (smwbrowse): node=%s, properties=%d", + $nodeTitleText, + count( $ret ) + ) ); + + return array_unique( $ret ); + } + /** * @param Title|MediaWiki\Title\Title $title $title * @return string|null @@ -403,6 +493,9 @@ public static function onOutputPageParserOutput( OutputPage $out, ParserOutput $ $out->addJsConfigVars( [ 'knowledgegraphs' => json_encode( $data ) ] ); + + // add the required JavaScript module if graphs are present + $out->addModules( 'ext.KnowledgeGraph' ); } } @@ -534,7 +627,16 @@ public static function setSemanticDataFromApi( Title $title, $onlyProperties, $d return; } - if ( $depth > $maxDepth ) { + // If maxDepth is 0, only create the root node without loading SMW data + if ( $maxDepth === 0 ) { + self::$data[$titleText] = [ + 'properties' => [], + 'categories' => [], + ]; + return; + } + + if ( $depth >= $maxDepth ) { return; } @@ -640,8 +742,19 @@ public static function setSemanticDataFromApi( Title $title, $onlyProperties, $d foreach ( $entry['dataitem'] ?? [] as $item ) { if ( $item['type'] === 9 ) { - $linkedTitle = explode( '#', $item['item'] )[0]; - $linkedTitle = $linkedTitle ? str_replace( '_', ' ', $linkedTitle ) : null; + $parts = explode( '#', $item['item'] ); + $dbkey = $parts[0] ?? ''; + $nsId = isset( $parts[1] ) && is_numeric( $parts[1] ) ? (int)$parts[1] : 0; + + $namespaceInfo = MediaWiki\MediaWikiServices::getInstance()->getNamespaceInfo(); + $nsName = $namespaceInfo->getCanonicalName( $nsId ); + + $linkedTitle = $dbkey; + if ( $nsName !== '' && $nsName !== false ) { + $linkedTitle = $nsName . ':' . $dbkey; + } + + $linkedTitle = str_replace( '_', ' ', $linkedTitle ); if ( !$linkedTitle ) { continue; } diff --git a/includes/api/KnowledgeGraphApiLoadNodes.php b/includes/api/KnowledgeGraphApiLoadNodes.php index c3c846d..f3f2ca7 100644 --- a/includes/api/KnowledgeGraphApiLoadNodes.php +++ b/includes/api/KnowledgeGraphApiLoadNodes.php @@ -8,7 +8,6 @@ */ use MediaWiki\Extension\KnowledgeGraph\Aliases\Title as TitleClass; -use MediaWiki\MediaWikiServices; class KnowledgeGraphApiLoadNodes extends ApiBase { @@ -91,7 +90,7 @@ public function mustBePosted(): bool { "_INST", "_PPGR", "_SUBP", - "_SUBC" + "_SUBC", ]; /** @@ -101,91 +100,27 @@ public function execute() { $result = $this->getResult(); $params = $this->extractRequestParams(); $context = $this->getContext(); - $output = $context->getOutput(); \KnowledgeGraph::initSMW(); self::$SMWStore = \SMW\StoreFactory::getStore(); self::$SMWDataValueFactory = SMW\DataValueFactory::getInstance(); - $services = MediaWikiServices::getInstance(); - $urlUtils = $services->getUrlUtils(); - $httpRequestFactory = $services->getHttpRequestFactory(); - - $scriptPath = $services->getMainConfig()->get( 'ScriptPath' ); - $server = $services->getMainConfig()->get( 'Server' ); - $apiUrl = $server . $scriptPath . '/api.php'; - - $queryParams = [ - 'action' => 'query', - 'list' => 'allpages', - 'apnamespace' => 102, - 'aplimit' => 'max', - 'format' => 'json' - ]; - - $query = http_build_query( $queryParams ); - $response = $httpRequestFactory->get( "$apiUrl?$query", [], __METHOD__ ); - $data = json_decode( $response, true ); - - $propertyTitles = array_column( $data['query']['allpages'], 'title' ); - $propertyNames = array_map( static function ( $title ) { - return substr( $title, strrpos( $title, ':' ) + 1 ); - }, $propertyTitles ); - - $params['properties'] = ( !empty( $params['properties'] ) ? - json_decode( $params['properties'], true ) : [] ); - $titles = explode( '|', $params['titles'] ); foreach ( $titles as $titleText ) { $title_ = TitleClass::newFromText( $titleText ); - - foreach ( $propertyNames as $propertyName ) { - $propertyDI = \SMW\DIProperty::newFromUserLabel( $propertyName ); - $results = \KnowledgeGraph::getSubjectsByProperty( $propertyDI, $limit, 0, $titleText ); - if ( count( $results ) > 0 ) { - $params['properties'][] = $propertyName; - } + if ( !$title_ || !$title_->isKnown() ) { + continue; } - $subject = new \SMW\DIWikiPage( $title_->getDbKey(), $title_->getNamespace() ); - $semanticData = self::$SMWStore->getSemanticData( $subject ); - - foreach ( $semanticData->getProperties() as $property ) { - $key = $property->getKey(); - - $typeID = $property->findPropertyTypeID(); - - if ( in_array( $key, self::$exclude ) ) { - continue; - } - - $propertyDv = self::$SMWDataValueFactory->newDataValueByItem( $property, null ); - if ( !$property->isUserAnnotable() || !$propertyDv->isVisible() ) { - continue; - } - - $key = str_replace( '_', ' ', $property->getKey() ); - - $params['properties'][] = $key; - - } + $listOfProps = \KnowledgeGraph::getAllPropertiesForNode( $titleText ); - $params['properties'] = array_unique( $params['properties'] ); - - $params['properties'] = array_unique( - array_merge( - $params['properties'], - array_map( - fn ( $prop ) => '-' . $prop, - $params['properties'] - ) - ) - ); - - if ( $title_ && $title_->isKnown() ) { - if ( !isset( self::$data[$title_->getFullText()] ) ) { - \KnowledgeGraph::setSemanticDataFromApi( $title_, $params['properties'], 0, $params['depth'] ); - } + if ( !isset( self::$data[$title_->getFullText()] ) ) { + \KnowledgeGraph::setSemanticDataFromApi( + $title_, + $listOfProps, + 0, + $params['depth'] + ); } } diff --git a/includes/specials/SpecialKnowledgeGraphDesigner.php b/includes/specials/SpecialKnowledgeGraphDesigner.php index 42e615c..edfe734 100644 --- a/includes/specials/SpecialKnowledgeGraphDesigner.php +++ b/includes/specials/SpecialKnowledgeGraphDesigner.php @@ -68,7 +68,8 @@ public function execute( $par ) { 'knowledgegraphs' => json_encode( \KnowledgeGraph::$graphs ), 'KnowledgeGraphShowImages' => $GLOBALS['wgKnowledgeGraphShowImages'], 'KnowledgeGraphDisableCredits' => $GLOBALS['wgKnowledgeGraphDisableCredits'], - 'wgKnowledgeGraphColorPalette' => $colors + 'wgKnowledgeGraphColorPalette' => $colors, + 'wgExtraNamespaces' => $GLOBALS['wgExtraNamespaces'] ] ); $out->addHTML( diff --git a/resources/KnowledgeGraph.css b/resources/KnowledgeGraph.css index a3d3dce..ba25f79 100644 --- a/resources/KnowledgeGraph.css +++ b/resources/KnowledgeGraph.css @@ -10,7 +10,7 @@ border: 1px solid #ccc; height: auto; background: white; - box-shadow: 0px 2px 18px 0px rgba(0,0,0,0.12); + box-shadow: 0px 2px 18px 0px rgba(0, 0, 0, 0.12); } .KnowledgeGraphPopupMenu ul { @@ -24,14 +24,14 @@ padding: 8px; list-style: none; cursor: pointer; - } + .KnowledgeGraphPopupMenu li span { margin-right: 8px; } .KnowledgeGraphPopupMenu li:hover { - background: #e8e8e9; + background: #e8e8e9; } .KnowledgeGraphTable .vis-configuration-wrapper { @@ -48,12 +48,12 @@ .OOUI-dialogs-non-modal .oo-ui-window { border: 1px solid #a2a9b1; - box-shadow: 0 0 4px 0 rgba( 0, 0, 0, 0.25 ); - position: relative; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25); + position: relative; margin: 0 32px; } -.KnowledgeGraphTable .oo-ui-toolbar-position-top > .oo-ui-toolbar-bar { +.KnowledgeGraphTable .oo-ui-toolbar-position-top>.oo-ui-toolbar-bar { border-bottom: none; box-shadow: none; } @@ -66,26 +66,26 @@ .kg-node-properties-menu li.kg-node-properties-menu-link-entry:hover, .kg-node-properties-menu li.kg-node-properties-menu-property-entry:hover, .kg-node-properties-menu li.kg-node-properties-menu-edge-entry:hover { - background-color: #e0f0ff; - cursor: pointer; + background-color: #e0f0ff; + cursor: pointer; } .kg-node-properties-menu { - position: absolute; - background: #fff; - border: 1px solid #ccc; - padding: 5px; - list-style: none; - z-index: 10000; - max-height: 300px; - overflow-y: auto; - margin: 0; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); - cursor: pointer; + position: absolute; + background: #fff; + border: 1px solid #ccc; + padding: 5px; + list-style: none; + z-index: 10000; + max-height: 300px; + overflow-y: auto; + margin: 0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + cursor: pointer; } .kg-node-properties-menu-property-entry-selected { - font-style: italic; - font-weight: bold; - color: #2B7CE9; -} + font-style: italic; + font-weight: bold; + color: #2B7CE9; +} \ No newline at end of file diff --git a/resources/KnowledgeGraph.js b/resources/KnowledgeGraph.js index 6c0dbbc..53686f6 100644 --- a/resources/KnowledgeGraph.js +++ b/resources/KnowledgeGraph.js @@ -7,50 +7,70 @@ */ KnowledgeGraph = function () { - var Nodes; - var Edges; - var Data = {}; - var maxPropValueLength = 20; - var Config; - var Container; - var Properties = {}; - // var ModelProperties = {}; - var SelectedNode = null; - var TmpData; - var Network; - var PopupMenuId = 'knowledgegraphp-popup-menu'; - var InitialData; - var ContainerOptions; - var WindowManagerNonModal; - var DialogCredits = 'dialog-credits'; - var PropColors = {}; - var Categories = {}; - var LegendDiv; - var PropIdPropLabelMap = {}; - var nodePropertiesCache = {}; - const colors = mw.config.get('wgKnowledgeGraphColorPalette'); + // instance bag + const self = {}; + + // instance state (defaults) + self.Nodes = null; + self.Edges = null; + self.Data = {}; + self.maxPropValueLength = 20; + self.Config = null; + self.Container = null; + self.Properties = {}; + self.SelectedNode = null; + self.TmpData = null; + self.Network = null; + self.PopupMenuId = 'knowledgegraphp-popup-menu'; + self.InitialData = null; + self.ContainerOptions = null; + self.WindowManagerNonModal = null; + self.DialogCredits = 'dialog-credits'; + self.PropColors = {}; + self.Categories = {}; + self.LegendDiv = null; + self.PropIdPropLabelMap = {}; + self.nodePropertiesCache = {}; + self.id = null; + self.colors = mw.config.get('wgKnowledgeGraphColorPalette'); function addLegendEntry(id, label, color) { - if (!LegendDiv) return; - if ($(LegendDiv).find('#' + id.replace(/ /g, '_')).length) { + if (!self.LegendDiv) return; + + const safeId = id.replace(/ /g, '_'); + const uniqueId = `${self.id}-${safeId}`; + + if (self.LegendDiv.querySelector(`#${CSS.escape(uniqueId)}`)) { return; } - let fontColor = KnowledgeGraphFunctions.getContrastColor(color); // koristi istu WCAG logiku + + let fontColor = KnowledgeGraphFunctions.getContrastColor(color); if (!fontColor) fontColor = '#000000'; - - var container = document.createElement('button'); - container.className = 'legend-element-container'; - container.classList.add('btn', 'btn-outline-light'); - container.id = id.replace(/ /g, '_'); + + const container = document.createElement('button'); + container.className = 'legend-element-container btn btn-outline-light'; + container.id = uniqueId; container.style.color = fontColor; container.style.background = color; container.innerHTML = label; - container.innerHTML = id; + container.innerHTML = id; container.dataset.active = true; container.dataset.active_color = color; - LegendDiv.append(container); + self.LegendDiv.append(container); + } + + function removeLegendEntry(property) { + if (!self.LegendDiv) return; + // use instance-specific ID + const safeId = `${this.id}-${property.replace(/ /g, '_')}`; + const entry = this.LegendDiv.querySelector(`#${CSS.escape(safeId)}`); + + if (entry) { + entry.remove(); + console.debug(`Legend entry removed for ${property} in ${this.id}`); + } } function checkAndToogleId(id) { @@ -58,46 +78,45 @@ KnowledgeGraph = function () { } function dispatchEvent_LegendClick(event, id) { - var container = $(LegendDiv).find('#' + id.replace(/ /g, '_'))[0]; - if (container.dataset.active === 'true') { - container.dataset.active = false; + if (!this.LegendDiv) return; + + const safeId = `${this.id}-${id.replace(/ /g, '_')}`; + const container = this.LegendDiv.querySelector(`#${CSS.escape(safeId)}`); + if (!container) return; + + const isActive = container.dataset.active === 'true'; + container.dataset.active = (!isActive).toString(); + + if (isActive) { container.style.background = '#FFFFFF'; - - let bgColor = container.style.background; - let fontColor = KnowledgeGraphFunctions.getContrastColor(bgColor); - if (!fontColor) fontColor = '#000000'; + const fontColor = KnowledgeGraphFunctions.getContrastColor(container.style.background) || '#000000'; container.style.color = fontColor; - } else { - container.dataset.active = true; container.style.background = container.dataset.active_color; - - let bgColor = container.style.background; - let fontColor = KnowledgeGraphFunctions.getContrastColor(bgColor); - if (!fontColor) fontColor = '#000000'; + const fontColor = KnowledgeGraphFunctions.getContrastColor(container.style.background) || '#000000'; container.style.color = fontColor; } - var updateNodes = []; - var visited = []; + + const updateNodes = []; + const visited = []; + const self = this; function toggleConnectedNodes(nodeId) { - if (visited.indexOf(nodeId) !== -1) { - return; - } + if (visited.includes(nodeId)) return; visited.push(nodeId); - var connectedNodes = Network.getConnectedNodes(nodeId); - - for (var nodeId_ of connectedNodes) { - var connectedEdgesIds = Network.getConnectedEdges(nodeId_); - var connectedEdges = Edges.get(connectedEdgesIds); + const connectedNodes = self.Network.getConnectedNodes(nodeId); + for (const nodeId_ of connectedNodes) { + const connectedEdgesIds = self.Network.getConnectedEdges(nodeId_); + const connectedEdges = self.Edges.get(connectedEdgesIds); - var found = false; - connectedEdges.forEach((edge) => { + let found = false; + for (const edge of connectedEdges) { if (edge.to === nodeId || edge.from === nodeId) { found = true; + break; } - }); + } if (!found) { updateNodes.push({ @@ -109,12 +128,17 @@ KnowledgeGraph = function () { } } - Nodes.forEach((node) => { - var idValue = checkAndToogleId(node.id); - if (PropIdPropLabelMap[id] === undefined) { - PropIdPropLabelMap[id] = []; + this.Nodes.forEach((node) => { + const idValue = checkAndToogleId(node.id); + + if (this.PropIdPropLabelMap[id] === undefined) { + this.PropIdPropLabelMap[id] = []; } - if (PropIdPropLabelMap[id].indexOf(idValue) !== -1 || PropIdPropLabelMap[id].indexOf(node.id) !== -1) { + + if ( + this.PropIdPropLabelMap[id].includes(idValue) || + this.PropIdPropLabelMap[id].includes(node.id) + ) { updateNodes.push({ id: node.id, hidden: container.dataset.active === 'true' ? false : true, @@ -123,22 +147,21 @@ KnowledgeGraph = function () { } }); - Nodes.update(updateNodes); + this.Nodes.update(updateNodes); } function deleteNode(nodeId) { - var children = Network.getConnectedNodes(nodeId); - children = children.filter( - (x) => !(x in Data) || Network.getConnectedNodes(x).length === 1 + const children = self.Network.getConnectedNodes(nodeId).filter( + (x) => !(x in self.Data) || self.Network.getConnectedNodes(x).length === 1 ); children.push(nodeId); - for (var nodeId of children) { - Edges.remove(Network.getConnectedEdges(nodeId)); + for (const nid of children) { + self.Edges.remove(self.Network.getConnectedEdges(nid)); } - Nodes.remove(children); - for (var nodeId of children) { - delete Data[nodeId]; + self.Nodes.remove(children); + for (const nid of children) { + delete self.Data[nid]; } } @@ -148,7 +171,7 @@ KnowledgeGraph = function () { action: 'knowledgegraph-load-nodes', titles: obj.title, depth: obj.depth, - properties: JSON.stringify(Config['properties']), + properties: JSON.stringify(self.Config['properties']), }; } else if (obj.properties !== null) { if (obj.properties === undefined) { @@ -161,7 +184,7 @@ KnowledgeGraph = function () { depth: obj.depth, limit: obj.limit, offset: obj.offset, - inversePropsIncluded: inversePropsIncluded + inversePropsIncluded: inversePropsIncluded, }; } else if (obj.categories !== null) { var payload = { @@ -248,27 +271,25 @@ KnowledgeGraph = function () { } function addArticleNode(data, label, options, typeID) { - if (Nodes.get(label) !== null) { + if (self.Nodes.get(label) !== null) { return; } let cleanLabel = label.split('#')[0]; - var nodeConfig = jQuery.extend( - JSON.parse(JSON.stringify(Config.graphOptions.nodes)), - label in Config.propertyOptions ? Config.propertyOptions[label] : {}, + const nodeConfig = jQuery.extend( + JSON.parse(JSON.stringify(self.Config.graphOptions.nodes)), + label in self.Config.propertyOptions ? self.Config.propertyOptions[label] : {}, { id: label, label: - cleanLabel.length <= maxPropValueLength + cleanLabel.length <= self.maxPropValueLength ? cleanLabel : wrapLabel(cleanLabel, 20), shape: 'box', - font: jQuery.extend( - {}, - Config.graphOptions.nodes.font, - { size: Config.graphOptions.nodes.font.size || 30 } - ), + font: jQuery.extend({}, self.Config.graphOptions.nodes.font, { + size: self.Config.graphOptions.nodes.font.size || 30, + }), typeID: typeID || 9, // https://visjs.github.io/vis-network/examples/network/other/popups.html @@ -285,6 +306,7 @@ KnowledgeGraph = function () { if (data[label] === null) { nodeConfig.opacity = 0.5; + nodeConfig.shapeProperties = nodeConfig.shapeProperties || {}; nodeConfig.shapeProperties.borderDashes = [5, 5]; } @@ -297,12 +319,12 @@ KnowledgeGraph = function () { nodeConfig.image = data[label].src; } - Nodes.add(nodeConfig); + self.Nodes.add(nodeConfig); } function createNodes(data) { - for (var label in data) { - if (label in Data && Data[label] !== null) { + for (const label in data) { + if (label in self.Data && self.Data[label] !== null) { continue; } @@ -312,34 +334,34 @@ KnowledgeGraph = function () { continue; } - if (!(label in Categories)) { - Categories[label] = []; + if (!(label in self.Categories)) { + self.Categories[label] = []; } - for (var i in data[label].categories) { - var category = data[label].categories[i]; - if (Categories[label].indexOf(category) === -1) { - Categories[label].push(category); + for (const i in data[label].categories) { + const category = data[label].categories[i]; + if (self.Categories[label].indexOf(category) === -1) { + self.Categories[label].push(category); } } - for (var i in data[label].properties) { - var property = data[label].properties[i]; + for (const i in data[label].properties) { + const property = data[label].properties[i]; - if (!(property.canonicalLabel in PropColors)) { - if (colors && colors.length > 0) { + if (!(property.canonicalLabel in self.PropColors)) { + if (self.colors && self.colors.length > 0) { // use d3 palette colors defined in wgKnowledgeGraphColorPalette - PropColors[property.canonicalLabel] = KnowledgeGraphFunctions.colorForPropertyLabel( + self.PropColors[property.canonicalLabel] = KnowledgeGraphFunctions.colorForPropertyLabel( property.canonicalLabel, - colors, - PropColors + self.colors, + self.PropColors ); } else { // use random HSL colors if no palette defined let color_; function colorExists() { - for (let j in PropColors) { - if (PropColors[j] === color_) { + for (const j in self.PropColors) { + if (self.PropColors[j] === color_) { return true; } } @@ -348,22 +370,22 @@ KnowledgeGraph = function () { do { color_ = KnowledgeGraphFunctions.randomHSL(); } while (colorExists()); - PropColors[property.canonicalLabel] = color_; + self.PropColors[property.canonicalLabel] = color_; } } - var options = - property.preferredLabel in Config.propertyOptions - ? Config.propertyOptions[property.preferredLabel] - : property.canonicalLabel in Config.propertyOptions - ? Config.propertyOptions[property.canonicalLabel] + let options = + property.preferredLabel in self.Config.propertyOptions + ? self.Config.propertyOptions[property.preferredLabel] + : property.canonicalLabel in self.Config.propertyOptions + ? self.Config.propertyOptions[property.canonicalLabel] : {}; if ('nodes' in options) { options = options.nodes; } if (!('color' in options)) { - const nodeColor = PropColors[property.canonicalLabel]; + const nodeColor = self.PropColors[property.canonicalLabel]; const textColor = KnowledgeGraphFunctions.getContrastColor(nodeColor); options.color = { @@ -371,71 +393,58 @@ KnowledgeGraph = function () { border: '#333', highlight: { background: nodeColor, - border: '#000' - } + border: '#000', + }, }; - // ensure readable font color when node background is dark + // readable font color when background dark options.font = Object.assign({}, options.font, { - color: textColor + color: textColor, }); } - var legendLabel = + const legendLabel = property.preferredLabel !== '' ? property.preferredLabel : property.canonicalLabel; - if (!(legendLabel in PropIdPropLabelMap)) { - PropIdPropLabelMap[legendLabel] = []; + if (!(legendLabel in self.PropIdPropLabelMap)) { + self.PropIdPropLabelMap[legendLabel] = []; } - var propLabel = - legendLabel + - (!Config['show-property-type'] - ? '' - : ' (' + property.typeLabel + ')'); - - if (Config['properties-panel']) { - addLegendEntry( - property.canonicalLabel, - legendLabel, - PropColors[property.canonicalLabel] - ); + const propLabel = + legendLabel + (!self.Config['show-property-type'] ? '' : ' (' + property.typeLabel + ')'); + + if (self.Config['properties-panel']) { + addLegendEntry(property.canonicalLabel, legendLabel, self.PropColors[property.canonicalLabel]); } switch (property.typeId) { case '_wpg': - for (var ii in property.values) { - var targetLabel = property.values[ii].value; - PropIdPropLabelMap[legendLabel].push(targetLabel); + for (const ii in property.values) { + const targetLabel = property.values[ii].value; + self.PropIdPropLabelMap[legendLabel].push(targetLabel); - var from = property.inverse ? targetLabel : label; - var to = property.inverse ? label : targetLabel; + const from = property.inverse ? targetLabel : label; + const to = property.inverse ? label : targetLabel; - let edgeId = KnowledgeGraphFunctions.makeEdgeId(from, to, property.canonicalLabel, 9, Nodes); + const edgeId = KnowledgeGraphFunctions.makeEdgeId(from, to, property.canonicalLabel, 9, self.Nodes); - var edgeConfig = jQuery.extend( - JSON.parse(JSON.stringify(Config.graphOptions.edges)), + const edgeConfig = jQuery.extend( + JSON.parse(JSON.stringify(self.Config.graphOptions.edges)), { id: edgeId, from: from, to: to, label: propLabel, group: label, - arrows: { - to: { enabled: true } - } + arrows: { to: { enabled: true } }, } ); - // Edges.add(edgeConfig); - graphModel.addEdge(edgeConfig); + self.graphModel.addEdge(edgeConfig); - if ( - property.values[ii].src && - mw.config.get('KnowledgeGraphShowImages') === true - ) { + if (property.values[ii].src && mw.config.get('KnowledgeGraphShowImages') === true) { options.shape = 'image'; options.image = property.values[ii].src; } @@ -445,66 +454,67 @@ KnowledgeGraph = function () { break; default: - const seen = new Set(); - for (const { value: targetLabel } of property.values) { - if (seen.has(targetLabel)) continue; - seen.add(targetLabel); - - const typeId = property.typeId === '_txt' ? 2 : property.typeId; - const valueId = KnowledgeGraphFunctions.makeNodeId(targetLabel, typeId); - const edgeLabel = property.canonicalLabel || propLabel; - - PropIdPropLabelMap[legendLabel].push(valueId); - - const edgeId = KnowledgeGraphFunctions.makeEdgeId(label, valueId, edgeLabel); - Edges.add({ - id: edgeId, - from: label, - to: valueId, - label: propLabel, - group: label, - }); + { + const seen = new Set(); + for (const { value: targetLabel } of property.values) { + if (seen.has(targetLabel)) continue; + seen.add(targetLabel); + + const typeId = property.typeId === '_txt' ? 2 : property.typeId; + const valueId = KnowledgeGraphFunctions.makeNodeId(targetLabel, typeId); + const edgeLabel = property.canonicalLabel || propLabel; + + self.PropIdPropLabelMap[legendLabel].push(valueId); - if (!Nodes.get(valueId)) { - const displayLabel = targetLabel.length <= maxPropValueLength - ? targetLabel - : wrapLabel(targetLabel, 20); - - Nodes.add( - jQuery.extend({}, options, { - id: valueId, - label: displayLabel, - typeID: typeId, - }) - ); + const edgeId = KnowledgeGraphFunctions.makeEdgeId(label, valueId, edgeLabel); + self.Edges.add({ + id: edgeId, + from: label, + to: valueId, + label: propLabel, + group: label, + }); + + if (!self.Nodes.get(valueId)) { + const displayLabel = targetLabel.length <= self.maxPropValueLength + ? targetLabel + : wrapLabel(targetLabel, 20); + + self.Nodes.add( + jQuery.extend({}, options, { + id: valueId, + label: displayLabel, + typeID: typeId, + }) + ); + } } } - } } } - Data = jQuery.extend(Data, data); + } + self.Data = jQuery.extend(self.Data, data); } function HideNodesRec(nodeId) { - var children = Network.getConnectedNodes(nodeId); - // children = children.filter((x) => excludedIds.indexOf(x) === -1); - var updateNodes = []; - for (var nodeId_ of children) { - if (!(nodeId_ in Data)) { + const children = self.Network.getConnectedNodes(nodeId); + const updateNodes = []; + for (const nodeId_ of children) { + if (!(nodeId_ in self.Data)) { updateNodes.push({ id: nodeId_, - hidden: !Nodes.get(nodeId_).hidden, + hidden: !self.Nodes.get(nodeId_).hidden, }); } } - Nodes.update(updateNodes); + self.Nodes.update(updateNodes); } function getDialogActionProcessCallback(thisDialog, getActionProcess, action) { switch (action) { case 'delete': if (confirm(mw.msg('knowledgegraph-delete-node-confirm'))) { - deleteNode(SelectedNode); + deleteNode(self.SelectedNode); return new OO.ui.Process(function () { thisDialog.close({ action: action }); }); @@ -513,21 +523,21 @@ KnowledgeGraph = function () { case 'done': return new OO.ui.Process(function () { thisDialog.close({ action: action }).then(function () { - // createNodes(TmpData); + // createNodes(self.TmpData); }); - createNodes(TmpData); - TmpData = {}; + createNodes(self.TmpData); + self.TmpData = {}; }); case 'continue': return getActionProcess .call(thisDialog, action) .next(function () { return new Promise((resolve, reject) => { - var selectedTab = thisDialog.indexLayout.getCurrentTabPanelName(); - var titleValue = null; - var properties = null; - var categories = null; - var depth, limit, offset; + const selectedTab = thisDialog.indexLayout.getCurrentTabPanelName(); + let titleValue = null; + let properties = null; + let categories = null; + let depth, limit, offset; switch (selectedTab) { case 'by-article': @@ -537,11 +547,16 @@ KnowledgeGraph = function () { resolve(); return; } - var titleFullText = thisDialog.titleInputWidget - .getMWTitle() - .getPrefixedText(); + let ns = parseInt(thisDialog.namespaceDropdown.getValue() || 0, 10); + let titleObj = mw.Title.newFromText(titleValue, ns); + + if (!titleObj) { + resolve(); + return; + } + let titleFullText = titleObj.getPrefixedText(); - if (titleFullText in Data) { + if (titleFullText in self.Data) { thisDialog.actions.setMode('existing-node'); thisDialog.initializeResultsPanel('existing-node'); resolve(); @@ -563,14 +578,14 @@ KnowledgeGraph = function () { const newTitles = []; for (let i = 0; i < titles.length; i++) { - const titleObj = mw.Title.newFromText( titles[i] ); + const titleObj = mw.Title.newFromText(titles[i]); if (!titleObj) continue; const fullTitle = titleObj.getPrefixedText(); - if (fullTitle in Data) { - existingTitles.push( fullTitle ); + if (fullTitle in self.Data) { + existingTitles.push(fullTitle); } else { - newTitles.push( fullTitle ); + newTitles.push(fullTitle); } } @@ -610,24 +625,19 @@ KnowledgeGraph = function () { offset: parseInt(offset), }) .then(function (data) { - // Properties = data[titleFullText]; - TmpData = data; + self.TmpData = data; + let mode; if (selectedTab === 'by-article') { - var properties = data[titleFullText]; - var mode = Object.keys(properties).length - ? 'show-results' - : 'no-results'; + let ns = parseInt(thisDialog.namespaceDropdown.getValue() || 0, 10); + let titleObj = mw.Title.newFromText(titleValue, ns); + titleFullText = titleObj ? titleObj.getPrefixedText() : titleValue; + + let properties_ = data[titleFullText] || data[titleValue] || {}; + mode = Object.keys(properties_).length ? 'show-results' : 'no-results'; } else { - var mode = Object.keys(data).length - ? 'show-results' - : 'no-results'; + mode = Object.keys(data).length ? 'show-results' : 'no-results'; } - thisDialog.initializeResultsPanel( - mode, - selectedTab, - data, - selectedTab === 'by-article' ? titleFullText : null - ); + thisDialog.initializeResultsPanel(mode, selectedTab, data, selectedTab === 'by-article' ? titleFullText : null); thisDialog.actions.setMode(mode); resolve(); }) @@ -646,173 +656,92 @@ KnowledgeGraph = function () { } function getDialogOnSetupCallback(thisDialog, data) { - var self = thisDialog; if (data && data.nodeId) { - SelectedNode = data.nodeId; - var mode = 'edit'; - self.initializeResultsPanel(mode); - self.actions.setMode(mode); + self.SelectedNode = data.nodeId; + const mode = 'edit'; + thisDialog.initializeResultsPanel(mode); + thisDialog.actions.setMode(mode); } else { - self.actions.setMode('select'); + thisDialog.actions.setMode('select'); } } - function getDialogInitializeResultsPanel( - thisDialog, - mode, - selectedTab, - data, - titleFullText - ) { + function getDialogInitializeResultsPanel(thisDialog, mode, selectedTab, data, titleFullText) { + let $el; if (mode === 'no-results') { - var msg = mw.msg( - selectedTab === 'by-article' - ? 'knowledgegraph-dialog-results-no-properties' - : 'knowledgegraph-dialog-results-no-articles' - ); - + const msg = mw.msg(selectedTab === 'by-article' ? 'knowledgegraph-dialog-results-no-properties' : 'knowledgegraph-dialog-results-no-articles'); $el = $('' + msg + ''); } else if (mode === 'existing-node') { - $el = $( - '' + - mw.msg('knowledgegraph-dialog-results-existing-node') + - '' - ); + $el = $('' + mw.msg('knowledgegraph-dialog-results-existing-node') + ''); } else { $el = $('