diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7deeef88c..77d9e46b0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,42 +18,47 @@ jobs:
matrix:
include:
- mediawiki_version: '1.39'
- smw_version: dev-master
+ smw_version: 5.1.0
php_version: 8.1
+ mm_version: 6.0.1
database_type: mysql
- database_image: "mariadb:10"
+ database_image: "mariadb:11.2"
coverage: false
experimental: false
- mediawiki_version: '1.40'
- smw_version: dev-master
+ smw_version: 5.1.0
php_version: 8.1
+ mm_version: 6.0.1
database_type: mysql
database_image: "mariadb:11.2"
coverage: true
experimental: false
- mediawiki_version: '1.41'
- smw_version: dev-master
+ smw_version: 5.1.0
pf_version: 5.9
sfs_version: dev-master
php_version: 8.1
+ mm_version: 6.0.1
database_type: mysql
database_image: "mariadb:11.2"
coverage: false
experimental: false
- mediawiki_version: '1.42'
- smw_version: dev-master
+ smw_version: 5.1.0
pf_version: 5.9
sfs_version: dev-master
php_version: 8.1
+ mm_version: 6.0.1
database_type: mysql
database_image: "mariadb:11.2"
coverage: false
experimental: false
- - mediawiki_version: '1.43'
+ - mediawiki_version: '1.43.1'
smw_version: dev-master
pf_version: 5.9
sfs_version: dev-master
php_version: 8.1
+ mm_version: 6.0.1
database_type: mysql
database_image: "mariadb:11.2"
coverage: false
@@ -67,6 +72,7 @@ jobs:
PHP_VERSION: ${{ matrix.php_version }}
DB_TYPE: ${{ matrix.database_type }}
DB_IMAGE: ${{ matrix.database_image }}
+ MM_VERSION: ${{ matrix.mm_version }}
steps:
diff --git a/Makefile b/Makefile
index 06e370721..29b87e15b 100644
--- a/Makefile
+++ b/Makefile
@@ -14,13 +14,13 @@ EXTENSION=SemanticResultFormats
MW_VERSION?=1.39
PHP_VERSION?=8.1
DB_TYPE?=mysql
-DB_IMAGE?="mariadb:10"
+DB_IMAGE?="mariadb:11.2"
# extensions
-SMW_VERSION?=dev-master
+SMW_VERSION ?= 5.0.2
PF_VERSION ?= 5.5.1
SFS_VERSION ?= 4.0.0-beta
-MM_VERSION ?= 3.1.0
+MM_VERSION ?= 6.0.1
# composer
# Enables "composer update" inside of extension
diff --git a/README.md b/README.md
index 3fcb46ce5..bd16da4f2 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,7 @@ Semantic Result Formats (SRF) is a MediaWiki extension that provides extra visua
## Requirements
-- PHP 7.3.19 or later
-- MediaWiki 1.35 or later
-- Semantic MediaWiki 3.0 or later
+PHP, MediaWiki, and Semantic MediaWiki. For specific supported versions, see the [compatibility overview](docs/COMPATIBILITY.md).
## Installation
@@ -48,6 +46,9 @@ A list of people who have made contributions in the past can be found [here][con
This extension provides unit and integration tests and is usually run by a [continues integration platform][GitHub Actions]
but can also be executed locally using the shortcut command `composer phpunit` from the extension base directory.
+## For developers
+
+See the documention on how to [update d3chart JS library](https://github.com/SemanticMediaWiki/SemanticResultFormats/blob/master/formats/d3/README.md).
## License
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index a24819d94..aec6e66ff 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,37 +1,72 @@
These are the release notes for the [Semantic Result Formats](https://www.semantic-mediawiki.org/wiki/Extension:Semantic_Result_Formats) (a.k.a SRF) MediaWiki extension.
+## SRF 5.1.0
+
+Released on September 3, 2025.
+
+### Improvements to
+
+* Fixed compatibility issue with MediaWiki 1.43 in the `filtered` format
+* Added `sep` parameter to the `filtered` format to allow specifying the separator ([952](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/952))
+* Improved the `d3chart` format
+ * Refactored d3chart codebase to support newer D3 versions
+ * Bumped d3 from v3.0.4 to v6.7.0 ([951](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/951))
+ * Introduced package.json and modern build tooling to manage D3 as an npm dependency
+ * Updated D3 import and initialization to align with ES module standards
+ * Replaced legacy d3.v3.js with modern d3.min.js for better modularity and smaller bundle size
+ * Revised documentation to reflect new D3 usage and build process
+* Improved the `graph` format by adding `graphfieldspages` param to optionally include Page-type values as node fields ([958](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/958))
+* Fixed support for template names with spaces in the `datatables` format
+
## SRF 5.0.0
-Released on TDB
+Released on April 7, 2025.
+
+Highlights:
+* Support for MediaWiki 1.43 and Semantic MediaWiki 5
+* Significant enhancements to the DataTables format
-Compatibility changes
+### Compatibility Changes
* Added compatibility with Semantic MediaWiki 5.x
-* Changed minimum PHP version from 7.3 to 8.0
-* Improved PHP support up to version 8.4
-* Changed minimum MediaWiki version from 1.35 to 1.39
-* Improved MediaWiki support up to 1.43
+* Added support for PHP up to version 8.4
+* Added support for MediaWiki support up to version 1.43
+* Dropped support for MediaWiki older than 1.39
+* Dropped support for PHP older than 8.1
-Breaking changes
+### Breaking Changes
-*
+* Removed the Exhibit format
-New features and enhancements
+### New Features and Enhancements
-* [864](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/864) Updated DataTables from 1.13.2 to 2.1.8 (by @alistair3149)
+* Overhauled DataTables format
+ * Improved UI integration with MediaWiki by using [Codex](https://doc.wikimedia.org/codex/main/) [871](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/871), [879](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/879) (by @alistair3149)
+ * Added "show all" to DataTable length [868](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/868) (by @alistair3149)
+ * Fixed various sorting and filter issues [865](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/865) (by @alistair3149)
+ * Fixed template Ajax navigation and improved expansion [931](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/931) (by @thomas-topway-it for ([KM-A](https://km-a.net))
+ * Fixed missing [ParentProperties](https://www.semantic-mediawiki.org/wiki/Help:Subobjects_and_queries) values [833](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/833) and [825](https://github.com/SemanticMediaWiki/SemanticResultFormats/issues/825) (by @hkwi)
+ * Bump DataTables from 1.13.2 to 2.1.8 [864](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/864) (by @alistair3149)
+ * Improved client-side cache mechanism [904](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/904) (by @thomas-topway-it)
+ * Improved accessibility with `aria-live = "polite"` attribute in DataTable [845](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/845) (by @robinwiel)
+* Updated Filtered format dependencies: Fullcalendar 6 and leaflet 1.9/leaflet-providers 2.0 (by @thomas-topway-it for ([KM-A](https://km-a.net))
+* Added `prolog` format that generates SVO predicates
+* Added `dataframe` format
+* Improved `carousel` format with vertical centering, height adjustments, smarter captions, and nested slide [857](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/857) (by @thomas-topway-it for ([KM-A](https://km-a.net))
-Bug fixes
+### Bug Fixes
-*
+* Fixed precision issue in the `time` format
+* Fixed reuse of values output by the `incoming` format when no template is used
## SRF 4.2.1
Released on March 13, 2024.
-* [815](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/815) Document graphfields parameter to the graph format (by @alex-mashin)
+* [815](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/815) Document graphfields parameter to the graph format (by @alex-mashin)
* [806](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/806) Added required resources for the bubble chare to the jqplotchart format (by@YOUR1)
* [805](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/805) Fixed issue creating a lot of log spam (by @sophivorus)
-* [793](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/793) Fixed issue with the slidesToShow paramter to the carousel format (by @thomas-topway-it)
+* [793](https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/793) Fixed issue with the slidesToShow parameter to the carousel format (by @thomas-topway-it)
* Further improvements and fixes to the Continuous Integration (CI) (by [gesinn.it](https://gesinn.it))
* Updated translations (by translatewiki.net community)
diff --git a/Resources.php b/Resources.php
index fbf86b647..2c782e2f9 100644
--- a/Resources.php
+++ b/Resources.php
@@ -569,7 +569,7 @@
// D3
'ext.d3.core' => $moduleTemplate + [
- 'scripts' => 'resources/jquery/d3/d3.v3.js'
+ 'scripts' => 'resources/jquery/d3/d3.min.js'
],
//
@@ -780,7 +780,8 @@
],
'dependencies' => [
'ext.srf.filtered.calendar-view.messages',
- 'ext.jquery.fullcalendar'
+ // included using gulp
+ // 'ext.jquery.fullcalendar'
],
],
diff --git a/SemanticResultFormats.hooks.php b/SemanticResultFormats.hooks.php
index 1494d9308..4bb74e615 100644
--- a/SemanticResultFormats.hooks.php
+++ b/SemanticResultFormats.hooks.php
@@ -94,6 +94,7 @@ public static function addToAdminLinks( ALTree &$admin_links_tree ) {
* @return true
*/
public static function onResourceLoaderGetConfigVars( &$vars ) {
+ // Powers srf.settings.get(), via ext.stf.js.
$vars['srf-config'] = [
'version' => SRF_VERSION,
'settings' => [
diff --git a/SemanticResultFormats.utils.php b/SemanticResultFormats.utils.php
index 8a51c4db4..5328a639f 100644
--- a/SemanticResultFormats.utils.php
+++ b/SemanticResultFormats.utils.php
@@ -27,21 +27,6 @@ public static function htmlProcessingElement( $isHtml = true ) {
);
}
- /**
- * Add JavaScript variables to the output
- *
- * @since 1.8
- */
- public static function addGlobalJSVariables() {
- $options = [
- 'srfgScriptPath' => $GLOBALS['srfgScriptPath'],
- 'srfVersion' => SRF_VERSION
- ];
-
- $requireHeadItem = [ 'srf.options' => $options ];
- SMWOutputs::requireHeadItem( 'srf.options', self::makeVariablesScript( $requireHeadItem ) );
- }
-
/**
* Returns semantic search link for the current query
*
diff --git a/build b/build
index 13d00b11a..3c2344c63 160000
--- a/build
+++ b/build
@@ -1 +1 @@
-Subproject commit 13d00b11a7743cb6a524c6ed577b8dea3913a62c
+Subproject commit 3c2344c631a9023b228fd9970c8daf0583b333e1
diff --git a/composer.json b/composer.json
index af1a0060e..31b3eaef2 100644
--- a/composer.json
+++ b/composer.json
@@ -44,9 +44,9 @@
"source": "https://github.com/SemanticMediaWiki/SemanticResultFormats"
},
"require": {
- "php": ">=7.3",
+ "php": ">=8.1",
"composer/installers": ">=1.0.1",
- "nicmart/tree": "^0.2.7",
+ "nicmart/tree": "^0.4.0",
"data-values/geo": "~4.0|~3.0|~2.0"
},
"suggest": {
diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md
index d674a454c..12ebf0168 100644
--- a/docs/COMPATIBILITY.md
+++ b/docs/COMPATIBILITY.md
@@ -17,19 +17,26 @@ minimum requirements are indicated in bold.
Semantic MediaWiki |
Release status |
+
+ | 5.1.x |
+ 8.1 - 8.4 |
+ 1.39 - 1.44 |
+ 5.0 - 6.x |
+ Stable release |
+
| 5.0.x |
- 8.0 - 8.3+ |
- 1.39 - 1.43+ |
- 4.0.0 - 4.2+ |
- Future release |
+ 8.1 - 8.4 |
+ 1.39 - 1.43 |
+ 5.0 |
+ Obsolete release, no support |
| 4.2.x |
- 7.3 - 8.1 |
- 1.35 - 1.39+ |
- 3.2.x - 4.2+ |
- Stable release |
+ 7.3 - 8.2 |
+ 1.35 - 1.39 |
+ 3.2.x - 4.2 |
+ Obsolete release, no support |
| 4.1.x |
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index ec111501c..203b725ed 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -20,7 +20,7 @@ create one and add the following content to it:
```
{
"require": {
- "mediawiki/semantic-result-formats": "~4.2"
+ "mediawiki/semantic-result-formats": "~5.0"
}
}
```
@@ -28,7 +28,7 @@ create one and add the following content to it:
If you already have a "composer.local.json" file add the following line to the end of the "require"
section in your file:
- "mediawiki/semantic-result-formats": "~4.2"
+ "mediawiki/semantic-result-formats": "~5.0"
Remember to add a comma to the end of the preceding line in this section.
diff --git a/docs/README.md b/docs/README.md
index 898bcf8e9..ecc13d44d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,6 +8,7 @@
## Technical documentation
* [Filtered format](https://github.com/SemanticMediaWiki/SemanticResultFormats/blob/master/formats/filtered/README.md)
+* [D3chart format](https://github.com/SemanticMediaWiki/SemanticResultFormats/blob/master/formats/d3/README.md)
## Repository documentation
diff --git a/extension.json b/extension.json
index a83960540..a424484af 100644
--- a/extension.json
+++ b/extension.json
@@ -1,6 +1,6 @@
{
"name": "SemanticResultFormats",
- "version": "5.0.0-alpha",
+ "version": "5.1.0",
"author": [
"James Hong Kong",
"Stephan Gambke",
@@ -38,22 +38,22 @@
"SRF\\Tests\\": "tests/phpunit/"
},
"QUnitTestModule": {
- "localBasePath": "",
- "remoteExtPath": "SemanticResultFormats",
+ "localBasePath": "tests/qunit/",
+ "remoteExtPath": "SemanticResultFormats/tests/qunit/",
"scripts": [
- "tests/qunit/ext.srf.test.js",
- "tests/qunit/ext.srf.util.test.js",
- "tests/qunit/formats/ext.srf.formats.datatables.test.js",
- "tests/qunit/formats/ext.srf.formats.carousel.test.js",
- "tests/qunit/formats/ext.srf.formats.eventcalendar.tests.js",
- "tests/qunit/formats/ext.srf.formats.filtered.test.js",
- "tests/qunit/formats/ext.srf.formats.gallery.test.js",
- "tests/qunit/formats/ext.srf.formats.media.test.js",
- "tests/qunit/formats/ext.srf.formats.tagcloud.test.js",
- "tests/qunit/widgets/ext.srf.widgets.eventcalendar.tests.js",
- "tests/qunit/widgets/ext.srf.widgets.optionslist.test.js",
- "tests/qunit/widgets/ext.srf.widgets.panel.test.js",
- "tests/qunit/widgets/ext.srf.widgets.parameters.test.js"
+ "ext.srf.test.js",
+ "ext.srf.util.test.js",
+ "formats/ext.srf.formats.datatables.test.js",
+ "formats/ext.srf.formats.carousel.test.js",
+ "formats/ext.srf.formats.eventcalendar.tests.js",
+ "formats/ext.srf.formats.filtered.test.js",
+ "formats/ext.srf.formats.gallery.test.js",
+ "formats/ext.srf.formats.media.test.js",
+ "formats/ext.srf.formats.tagcloud.test.js",
+ "widgets/ext.srf.widgets.eventcalendar.tests.js",
+ "widgets/ext.srf.widgets.optionslist.test.js",
+ "widgets/ext.srf.widgets.panel.test.js",
+ "widgets/ext.srf.widgets.parameters.test.js"
],
"dependencies": [
"ext.srf",
diff --git a/formats/Prolog/PrologPrinter.php b/formats/Prolog/PrologPrinter.php
index 9c8a121d0..ab8c9fdbc 100644
--- a/formats/Prolog/PrologPrinter.php
+++ b/formats/Prolog/PrologPrinter.php
@@ -36,7 +36,7 @@ class PrologPrinter extends FileExportPrinter {
* {@inheritDoc}
*/
public function getName() {
- return $this->msg( 'srf-printername-prolog' );
+ return $this->msg( 'srf-printername-prolog' )->text();
}
/**
diff --git a/formats/calendar/SRF_Calendar.php b/formats/calendar/SRF_Calendar.php
index 6f80a4271..b8f634787 100644
--- a/formats/calendar/SRF_Calendar.php
+++ b/formats/calendar/SRF_Calendar.php
@@ -393,7 +393,7 @@ public function displayCalendar( $events ) {
$lastDayOfWeek = 7;
} else {
$firstDayOfWeek =
- array_search( $srfgFirstDayOfWeek, $weekDayNames );
+ array_search( wfMessage( $srfgFirstDayOfWeek )->text(), $weekDayNames );
if ( $firstDayOfWeek === false ) {
// Bad value for $srfgFirstDayOfWeek!
print 'Warning: Bad value for $srfgFirstDayOfWeek "' .
diff --git a/formats/d3/README.md b/formats/d3/README.md
new file mode 100644
index 000000000..d6f5d2905
--- /dev/null
+++ b/formats/d3/README.md
@@ -0,0 +1,42 @@
+# d3chart
+
+## Updating `d3chart` Library in `SemanticResultFormats`
+
+The version is managed via `npm` and bundled into the extension using a script.
+
+### How It Works
+
+After running `npm install`, the following happens automatically:
+
+1. **Copying `d3.min.js`**
+
+ A custom script (`copy-and-minify-d3.js`) copies the `d3.min.js` file from:
+ ```
+ node_modules/d3
+ ```
+
+ to:
+
+ ```
+ resources/jquery/d3/d3.min.js
+ ```
+
+### To Update `d3chart`
+
+1. Open `package.json` and change the version under `"d3"`
+
+ ```
+ "dependencies": {
+ "d3": "3.0.4"
+ }
+ ```
+2. Run:
+
+ ```bash
+ npm install
+ ```
+
+#### This will:
+
+* Download the new version of D3
+* Automatically copy the updated `d3.min.js` to the expected location via the script
\ No newline at end of file
diff --git a/formats/d3/resources/ext.srf.d3.chart.bubble.js b/formats/d3/resources/ext.srf.d3.chart.bubble.js
index cfde3de79..8ca13ce75 100644
--- a/formats/d3/resources/ext.srf.d3.chart.bubble.js
+++ b/formats/d3/resources/ext.srf.d3.chart.bubble.js
@@ -1,14 +1,16 @@
/**
- * JavaScript for SRF D3 chart bubble module using d3 v2
+ * JavaScript for SRF D3 chart bubble module
+ * Supports D3 v3 and v4+ (auto-detects version)
+ *
* @see http://www.semantic-mediawiki.org/wiki/Help:D3chart format
*
* @since 1.8
* @release 0.3
*
- * @licence GPL-2.0-or-later
+ * @license GPL-2.0-or-later
* @author mwjames
*/
-( function( $, srf ) {
+(function($, srf) {
'use strict';
/*global d3:true, mw:true, colorscheme:true*/
@@ -27,82 +29,146 @@
srf.formats.d3 = function() {};
srf.formats.d3.prototype = {
- bubble: function( context ) {
- return context.each( function() {
- var width = $( this ).width(),
- height = $( this ).height(),
- chart = $( this ).find( '.container' ),
- d3ID = chart.attr( 'id' ),
- json = mw.config.get( d3ID );
-
- // Parse json string and convert it back
- var container = typeof json === 'string' ? jQuery.parseJSON( json ) : json;
-
- var charttitle = container.parameters.charttitle,
- charttext = container.parameters.charttext,
- datalabels = container.parameters.datalabels,
- colors = container.parameters.colorscheme === null ? colorscheme[0] : colorscheme[container.parameters.colorscheme][9];
-
- // Release the graph
- util.spinner.hide( { context: $( this ) } );
- $( this ).css( 'width', width ).css( 'height', height);
+ bubble: function(context) {
+ return context.each(function() {
+ var width = $(this).width(),
+ height = $(this).height(),
+ chart = $(this).find('.container'),
+ d3ID = chart.attr('id'),
+ json = mw.config.get(d3ID);
+
+ var container = typeof json === 'string' ? jQuery.parseJSON(json) : json;
+
+ var charttitle = container.parameters.charttitle || '',
+ charttext = container.parameters.charttext || '',
+ datalabels = container.parameters.datalabels,
+ colors;
+
+ if (!container.parameters.colorscheme || typeof colorscheme[container.parameters.colorscheme] === 'undefined') {
+ colors = colorscheme[0];
+ } else {
+ colors = colorscheme[container.parameters.colorscheme];
+ }
+
+ // Hide spinner, set dimensions, show chart container
+ util.spinner.hide({ context: $(this) });
+ $(this).css('width', width).css('height', height);
chart.show();
- // Add chart title
- if ( charttitle.length > 0 ) {
- charttitle = '' + charttitle + '';
- $( this ).find( '#' + d3ID ).before( charttitle );
+ // Add chart title if set
+ if (charttitle.length > 0) {
+ var titleHTML = '' + charttitle + '';
+ $(this).find('#' + d3ID).before(titleHTML);
}
- // Add bottom chart text
- if ( charttext.length > 0 ) {
- charttext = '' + charttext + '';
- $( this ).find( '#' + d3ID ).after( charttext );
+ // Add chart text if set
+ if (charttext.length > 0) {
+ var textHTML = '' + charttext + '';
+ $(this).find('#' + d3ID).after(textHTML);
}
- // Calculate height
- height = height - ( $( this ).find( '.srf-d3-chart-title' ).height() + $( this ).find( '.srf-d3-chart-text' ).height() );
+ // Adjust height by subtracting title and text heights
+ var titleHeight = $(this).find('.srf-d3-chart-title').height() || 0;
+ var textHeight = $(this).find('.srf-d3-chart-text').height() || 0;
+ height = height - (titleHeight + textHeight);
+ if (isNaN(height) || height < 0) height = 0;
+
+ // Detect if using D3 v4+ by checking for d3.pack
+ var isV4Plus = typeof d3.pack === "function";
- // Create an ordinal color array and set formatting
- var color = d3.scale.ordinal().range( colors ),
- format = d3.format( ",d" );
+ // Color scale and format function
+ var color = isV4Plus
+ ? d3.scaleOrdinal().range(colors)
+ : d3.scale.ordinal().range(colors);
- // Data array definition
- var packlayout = [];
- packlayout.push( {
- label: charttitle !== '' ? container.parameters.charttitle : mw.config.get ( 'wgTitle' ),
+ var format = d3.format(",d");
+
+ // Data root object
+ var packlayout = {
+ label: charttitle !== '' ? charttitle : mw.config.get('wgTitle'),
children: container.data
- } );
-
- var pack = d3.layout.pack()
- .size([width - 4, height - 4])
- .value( function( d ) { return d.value; } );
-
- var vis = d3.select( "#" + d3ID ).append( "svg" )
- .attr( "width", width )
- .attr( "height", height )
- .attr( "class", "pack" )
- .append( "g" )
- .attr( "transform", "translate(2, 2)" );
-
- var node = vis.data(packlayout).selectAll("g.node")
- .data( pack.nodes )
- .enter().append("g")
- .attr( "class", function( d ) { return d.children ? "node" : "leaf node"; } )
- .attr( "transform", function( d ) { return "translate(" + d.x + "," + d.y + ")"; } );
-
- node.append("title")
- .text(function( d ) { return d.label + ( d.children ? "" : ": " + format( d.value ) ); } );
-
- node.append( "circle" )
- .attr( "r" , function( d ) { return d.r; } )
- .style( "fill" , function( d ) { return d.children ? null : color( d.label ); } );
-
- node.filter(function( d ) { return !d.children; } ).append("text")
- .attr( "text-anchor", "middle" )
- .attr( "dy", ".3em" )
- .text( function( d ) { return d.children ? null : datalabels === 'value' ? d.value : d.label.substring( 0, d.r / 3 ); } );
- } );
+ };
+
+ // Select or create SVG element
+ var svg = d3.select("#" + d3ID).select("svg");
+ if (svg.empty()) {
+ svg = d3.select("#" + d3ID).append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("class", "pack");
+ } else {
+ svg.selectAll("*").remove();
+ svg.attr("width", width).attr("height", height);
+ }
+
+ var vis = svg.append("g").attr("transform", "translate(2,2)");
+
+ if (isV4Plus) {
+ // D3 v4+ usage
+ var pack = d3.pack()
+ .size([width - 4, height - 4])
+ .padding(1);
+
+ var root = d3.hierarchy(packlayout)
+ .sum(function(d) { return d.value; })
+ .sort(function(a, b) { return b.value - a.value; });
+
+ pack(root);
+
+ var node = vis.selectAll("g.node")
+ .data(root.descendants())
+ .enter().append("g")
+ .attr("class", function(d) { return d.children ? "node" : "leaf node"; })
+ .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+
+ node.append("title")
+ .text(function(d) { return d.data.label + (d.children ? "" : ": " + format(d.value)); });
+
+ node.append("circle")
+ .attr("r", function(d) { return d.r; })
+ .style("fill", function(d) { return d.children ? null : color(d.data.label); });
+
+ node.filter(function(d) { return !d.children; })
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dy", ".3em")
+ .text(function(d) {
+ if (d.children) return null;
+ if (datalabels === 'value') return d.value;
+ return d.data.label.substring(0, d.r / 3);
+ });
+ } else {
+ // D3 v3 or lower usage
+ var pack = d3.layout.pack()
+ .size([width - 4, height - 4])
+ .value(function(d) { return d.value; });
+
+ var nodes = pack.nodes(packlayout);
+
+ var node = vis.selectAll("g.node")
+ .data(nodes)
+ .enter().append("g")
+ .attr("class", function(d) { return d.children ? "node" : "leaf node"; })
+ .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+
+ node.append("title")
+ .text(function(d) { return d.label + (d.children ? "" : ": " + format(d.value)); });
+
+ node.append("circle")
+ .attr("r", function(d) { return d.r; })
+ .style("fill", function(d) { return d.children ? null : color(d.label); });
+
+ node.filter(function(d) { return !d.children; })
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dy", ".3em")
+ .text(function(d) {
+ if (d.children) return null;
+ if (datalabels === 'value') return d.value;
+ return d.label.substring(0, d.r / 3);
+ });
+ }
+ });
}
};
@@ -115,8 +181,8 @@
var util = new srf.util();
$(document).ready(function() {
- $( '.srf-d3-chart-bubble' ).each(function() {
- srfD3.bubble( $( this ) );
- } );
- } );
-} )( jQuery, semanticFormats );
\ No newline at end of file
+ $('.srf-d3-chart-bubble').each(function() {
+ srfD3.bubble($(this));
+ });
+ });
+})(jQuery, semanticFormats);
diff --git a/formats/d3/resources/ext.srf.d3.chart.treemap.js b/formats/d3/resources/ext.srf.d3.chart.treemap.js
index 5165aadf3..4673c54ea 100644
--- a/formats/d3/resources/ext.srf.d3.chart.treemap.js
+++ b/formats/d3/resources/ext.srf.d3.chart.treemap.js
@@ -1,14 +1,14 @@
/**
- * JavaScript for SRF D3 chart treemap module using d3 v2
+ * JavaScript for SRF D3 chart treemap module supporting d3 v3 and v4+
* @see http://www.semantic-mediawiki.org/wiki/Help:D3chart format
*
* @since 1.8
* @release 0.3
*
- * @licence GPL-2.0-or-later
+ * @license GPL-2.0-or-later
* @author mwjames
*/
-( function( $, srf ) {
+(function($, srf) {
'use strict';
/*global d3:true, mw:true, colorscheme:true*/
@@ -28,100 +28,157 @@
srf.formats.d3 = function() {};
srf.formats.d3.prototype = {
- treemap: function( context ) {
- return context.each( function() {
- var width = $( this ).width(),
- height = $( this ).height(),
- chart = $( this ).find( '.container' ),
- d3ID = chart.attr( 'id' ),
- json = mw.config.get( d3ID );
-
- // Parse json string and convert it back
- var container = typeof json === 'string' ? jQuery.parseJSON( json ) : json;
-
- var charttitle = container.parameters.charttitle,
- charttext = container.parameters.charttext,
- datalabels = container.parameters.datalabels,
- colors = container.parameters.colorscheme === null ? colorscheme[0] : colorscheme[container.parameters.colorscheme][9];
-
- // Release the graph
- util.spinner.hide( { context: $( this ) } );
- $( this ).css( 'width', width ).css( 'height', height);
+ treemap: function(context) {
+ return context.each(function() {
+ var width = $(this).width(),
+ height = $(this).height(),
+ chart = $(this).find('.container'),
+ d3ID = chart.attr('id'),
+ json = mw.config.get(d3ID);
+
+ // Parse JSON string if necessary
+ var container = typeof json === 'string' ? jQuery.parseJSON(json) : json;
+
+ var charttitle = container.parameters.charttitle || '',
+ charttext = container.parameters.charttext || '',
+ datalabels = container.parameters.datalabels,
+ colors;
+
+ if (!container.parameters.colorscheme || typeof colorscheme[container.parameters.colorscheme] === 'undefined') {
+ colors = colorscheme[0];
+ } else {
+ colors = colorscheme[container.parameters.colorscheme];
+ }
+
+ // Hide spinner, set dimensions, show chart container
+ util.spinner.hide({ context: $(this) });
+ $(this).css('width', width).css('height', height);
chart.show();
- // Add chart title
- if ( charttitle.length > 0 ) {
- charttitle = '' + charttitle + '';
- $( this ).find( '#' + d3ID ).before( charttitle );
+ // Add chart title if set
+ if (charttitle.length > 0) {
+ var titleHTML = '' + charttitle + '';
+ $(this).find('#' + d3ID).before(titleHTML);
}
- // Add bottom chart text
- if ( charttext.length > 0 ) {
- charttext = '' + charttext + '';
- $( this ).find( '#' + d3ID ).after( charttext );
+ // Add chart text if set
+ if (charttext.length > 0) {
+ var textHTML = '' + charttext + '';
+ $(this).find('#' + d3ID).after(textHTML);
}
- // Calculate height
- height = height - ( $( this ).find( '.srf-d3-chart-title' ).height() + $( this ).find( '.srf-d3-chart-text' ).height() );
+ // Adjust height by subtracting title and text heights
+ var titleHeight = $(this).find('.srf-d3-chart-title').height() || 0;
+ var textHeight = $(this).find('.srf-d3-chart-text').height() || 0;
+ height = height - (titleHeight + textHeight);
+ if (isNaN(height) || height < 0) height = 0;
+
+ // Detect if using D3 v4+ by checking for d3.pack
+ var isV4Plus = typeof d3.pack === "function";
- // Create an ordinal color array and set formatting
- var color = d3.scale.ordinal().range( colors ),
- format = d3.format( ',d' );
+ // Color scale
+ var color = isV4Plus
+ ? d3.scaleOrdinal().range(colors)
+ : d3.scale.ordinal().range(colors);
- // Data array definition
- var treeArray = [];
- treeArray.push( {
- label: charttitle !== '' ? container.parameters.charttitle : mw.config.get ( 'wgTitle' ),
+ var format = d3.format(",d");
+
+ // Root data object
+ var treeData = {
+ label: charttitle !== '' ? charttitle : mw.config.get('wgTitle'),
children: container.data
- } );
-
- // Init layout
- var treemap = d3.layout.treemap()
- .padding( 4 )
- .size([ width , height ])
- .value( function( d ) { return d.value; } );
-
- var svg = d3.select( '#' + d3ID ).append( 'svg' )
- .attr( 'width', width )
- .attr( 'height', height )
- .append( 'g' )
- .attr( 'transform', 'translate(-.5,-.5)' );
-
- var cell = svg.data( treeArray ).selectAll( 'g' )
- .data( treemap )
- .enter().append( 'g' )
- .attr( 'class', 'cell' )
- .attr( 'transform', function( d ) { return 'translate(' + d.x + ',' + d.y + ')'; } );
-
- cell.append( 'title' )
- .text( function( d ) { return d.label + ( d.children ? '' : ': ' + format( d.value ) ); } );
-
- cell.append( 'rect' )
- .attr( 'width', function( d ) { return d.dx; } )
- .attr( 'height', function( d ) { return d.dy; } )
- .style( 'fill', function( d ) { return d.label ? color( d.label ) : color( d.label ); } );
-
- cell.append( 'text' )
- .attr( 'x', function( d ) { return d.dx / 2; } )
- .attr( 'y', function( d ) { return d.dy / 2; } )
- .attr( 'dy', '.35em' )
- .attr( 'text-anchor', 'middle' )
- .text( function( d ) { return d.children ? null : datalabels === 'value' ? d.value : d.label ; } );
- } );
+ };
+
+ // Select or create SVG container
+ var svg = d3.select("#" + d3ID).select("svg");
+ if (svg.empty()) {
+ svg = d3.select("#" + d3ID).append("svg")
+ .attr("width", width)
+ .attr("height", height);
+ } else {
+ svg.selectAll("*").remove();
+ svg.attr("width", width).attr("height", height);
+ }
+
+ if (isV4Plus) {
+ // D3 v4+ usage
+ var root = d3.hierarchy(treeData)
+ .sum(function(d) { return d.value; })
+ .sort(function(a, b) { return b.value - a.value; });
+
+ var treemap = d3.treemap()
+ .size([width, height])
+ .padding(4);
+
+ treemap(root);
+
+ var cell = svg.selectAll("g")
+ .data(root.leaves())
+ .enter().append("g")
+ .attr("class", "cell")
+ .attr("transform", function(d) { return "translate(" + d.x0 + "," + d.y0 + ")"; });
+
+ cell.append("title")
+ .text(function(d) { return d.data.label + ": " + format(d.value); });
+
+ cell.append("rect")
+ .attr("width", function(d) { return d.x1 - d.x0; })
+ .attr("height", function(d) { return d.y1 - d.y0; })
+ .style("fill", function(d) { return color(d.data.label); });
+
+ cell.append("text")
+ .attr("x", function(d) { return (d.x1 - d.x0) / 2; })
+ .attr("y", function(d) { return (d.y1 - d.y0) / 2; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function(d) {
+ if (datalabels === 'value') return d.value;
+ return d.data.label;
+ });
+ } else {
+ // D3 v3 or lower usage
+ var treemap = d3.layout.treemap()
+ .size([width, height])
+ .padding(4)
+ .value(function(d) { return d.value; });
+
+ var nodes = treemap.nodes(treeData);
+
+ var cell = svg.selectAll("g")
+ .data(nodes)
+ .enter().append("g")
+ .attr("class", "cell")
+ .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+
+ cell.append("title")
+ .text(function(d) { return d.label + (d.children ? "" : ": " + format(d.value)); });
+
+ cell.append("rect")
+ .attr("width", function(d) { return d.dx; })
+ .attr("height", function(d) { return d.dy; })
+ .style("fill", function(d) { return d.label ? color(d.label) : color(d.label); });
+
+ cell.append("text")
+ .attr("x", function(d) { return d.dx / 2; })
+ .attr("y", function(d) { return d.dy / 2; })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "middle")
+ .text(function(d) {
+ if (d.children) return null;
+ if (datalabels === 'value') return d.value;
+ return d.label;
+ });
+ }
+ });
}
};
- /**
- * Implementation and representation of the d3 treemap instance
- * @since 1.8
- * @type Object
- */
var srfD3 = new srf.formats.d3();
var util = new srf.util();
$(document).ready(function() {
- $( '.srf-d3-chart-treemap' ).each(function() {
- srfD3.treemap( $( this ) );
- } );
- } );
-} )( jQuery, semanticFormats );
\ No newline at end of file
+ $('.srf-d3-chart-treemap').each(function() {
+ srfD3.treemap($(this));
+ });
+ });
+})(jQuery, semanticFormats);
diff --git a/formats/dataframe/DataframePrinter.php b/formats/dataframe/DataframePrinter.php
index 4eec11cfd..96d1995ab 100644
--- a/formats/dataframe/DataframePrinter.php
+++ b/formats/dataframe/DataframePrinter.php
@@ -32,7 +32,7 @@ class DataframePrinter extends FileExportPrinter {
* {@inheritDoc}
*/
public function getName() {
- return $this->msg( 'srf-printername-dataframe' );
+ return $this->msg( 'srf-printername-dataframe' )->text();
}
/**
diff --git a/formats/datatables/DataTables.php b/formats/datatables/DataTables.php
index a15796a52..12632623f 100644
--- a/formats/datatables/DataTables.php
+++ b/formats/datatables/DataTables.php
@@ -13,6 +13,9 @@
namespace SRF;
use Html;
+use MediaWiki\MediaWikiServices;
+use Mediawiki\Title\Title;
+use MWException;
use Parser;
use RequestContext;
use SMW\DataValues\PropertyValue;
@@ -419,9 +422,13 @@ protected function buildResult( QueryResult $results ): array {
$this->isHTML = true;
$this->hasTemplates = false;
- $this->parser = $this->copyParser();
+ $outputMode = ( $this->params['apicall'] !== 'apicall' ? SMW_OUTPUT_HTML : $this->outputMode );
- $outputMode = ( $this->params['apicall'] !== "apicall" ? SMW_OUTPUT_HTML : $this->outputMode );
+ if ( $this->params['apicall'] === 'apicall' ) {
+ $this->initializePrintoutParametersAndParser( $results );
+ } else {
+ $this->parser = $this->copyParser();
+ }
// Get output from printer:
$result = $this->getResultText( $results, $outputMode );
@@ -435,6 +442,33 @@ protected function buildResult( QueryResult $results ): array {
return $result;
}
+ /**
+ * @param QueryResult $results
+ */
+ protected function initializePrintoutParametersAndParser( QueryResult $results ) {
+ // rebuild $this->printoutsParameters from
+ // printouts since $this->getPrintouts is not invoked
+ // alternatively use the $data['printouts'] from the Api
+ $printRequests = $results->getPrintRequests();
+ foreach ( $printRequests as $printRequest ) {
+ $canonicalLabel = $printRequest->getCanonicalLabel();
+ $this->printoutsParameters[$canonicalLabel] = $printRequest->getParameters();
+ }
+
+ // @see https://github.com/SemanticMediaWiki/SemanticResultFormats/pull/854
+ // the following ensures that $this->parser->recursiveTagParseFully
+ // (getCellContent) will work
+ $context = RequestContext::getMain();
+ $performer = $context->getUser();
+ $output = $context->getOutput();
+
+ $this->parser = MediaWikiServices::getInstance()->getParserFactory()->getInstance();
+ $this->parser->setTitle( $output->getTitle() );
+ $this->parser->setOptions( $output->parserOptions() );
+ $this->parser->setOutputType( Parser::OT_HTML );
+ $this->parser->clearState();
+ }
+
/**
* {@inheritDoc}
*/
@@ -462,6 +496,8 @@ protected function handleNonFileResult( $result, QueryResult $results, $outputmo
// Apply intro parameter
if ( ( $this->mIntro ) && ( $results->getCount() > 0 ) ) {
+ // @see https://github.com/SemanticMediaWiki/SemanticResultFormats/issues/853
+ // $result = $this->parser->recursiveTagParseFully( $this->mIntro ) . $result;
if ( $outputmode == SMW_OUTPUT_HTML && $this->isHTML ) {
$result = Message::get( [ 'smw-parse', $this->mIntro ], Message::PARSE ) . $result;
} elseif ( $outputmode !== SMW_OUTPUT_RAW ) {
@@ -471,6 +507,8 @@ protected function handleNonFileResult( $result, QueryResult $results, $outputmo
// Apply outro parameter
if ( ( $this->mOutro ) && ( $results->getCount() > 0 ) ) {
+ // @see https://github.com/SemanticMediaWiki/SemanticResultFormats/issues/853
+ // $result = $result . $this->parser->recursiveTagParseFully( $this->mOutro );
if ( $outputmode == SMW_OUTPUT_HTML && $this->isHTML ) {
$result = $result . Message::get( [ 'smw-parse', $this->mOutro ], Message::PARSE );
} elseif ( $outputmode !== SMW_OUTPUT_RAW ) {
@@ -878,7 +916,7 @@ private function getResultJson( QueryResult $res, int $outputMode ): array {
/**
* @see SMW\Query\ResultPrinters\TableResultPrinter
*/
- private function getCellContent(
+ public function getCellContent(
string $label,
array $dataValues,
int $outputMode,
@@ -931,9 +969,13 @@ private function getCellContent(
}
if ( $template ) {
- // escape pipe character
- $value_ = str_replace( '|', '|', (string)$value );
- $value = $this->parser->recursiveTagParseFully( '{{' . $template . '|' . $value_ . '}}' );
+ // @fixme use named parameter ?
+ $titleTemplate = Title::makeTitle( NS_TEMPLATE,
+ Title::capitalize( trim( $template ), NS_TEMPLATE ) );
+ $value_ = $this->expandTemplate( $titleTemplate, [ 1 => $value ] );
+ $value = Parser::stripOuterParagraph(
+ $this->parser->recursiveTagParseFully( $value_ )
+ );
}
$values[] = $value === '' ? ' ' : $value;
@@ -966,6 +1008,38 @@ private function getCellContent(
];
}
+ /**
+ * @see https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/VisualData/+/refs/heads/master/includes/classes/ResultPrinter.php
+ * @param Title|Mediawiki\Title\Title $title
+ * @param array $args
+ * @return array
+ */
+ public function expandTemplate( $title, $args ) {
+ $titleText = $title->getText();
+ $frame = $this->parser->getPreprocessor()->newFrame();
+
+ if ( $frame->depth >= $this->parser->getOptions()->getMaxTemplateDepth() ) {
+ throw new MWException( 'expandTemplate: template depth limit exceeded' );
+ }
+
+ if ( MediaWikiServices::getInstance()->getNamespaceInfo()->isNonincludable( $title->getNamespace() ) ) {
+ throw new MWException( 'expandTemplate: template inclusion denied' );
+ }
+
+ [ $dom, $finalTitle ] = $this->parser->getTemplateDom( $title );
+ if ( $dom === false ) {
+ throw new MWException( "expandTemplate: template \"$titleText\" does not exist" );
+ }
+
+ if ( !$frame->loopCheck( $finalTitle ) ) {
+ throw new MWException( 'expandTemplate: template loop detected' );
+ }
+
+ $fargs = $this->parser->getPreprocessor()->newPartNodeArray( $args );
+ $newFrame = $frame->newChild( $fargs, $finalTitle );
+ return $newFrame->expand( $dom );
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/formats/datatables/QuerySegmentListProcessor.php b/formats/datatables/QuerySegmentListProcessor.php
new file mode 100644
index 000000000..1ca726f57
--- /dev/null
+++ b/formats/datatables/QuerySegmentListProcessor.php
@@ -0,0 +1,390 @@
+joinConditions and $this->fromTables
+ * to the original QuerySegmentListProcessor for the
+ * use with SearchPanes
+ */
+namespace SRF\DataTables;
+
+use RuntimeException;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
+use SMWQuery as Query;
+
+class QuerySegmentListProcessor {
+ /* @var array */
+ public $joinConditions = [];
+
+ /* @var array */
+ public $fromTables = [];
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var TemporaryTableBuilder
+ */
+ private $temporaryTableBuilder;
+
+ /**
+ * @var HierarchyTempTableBuilder
+ */
+ private $hierarchyTempTableBuilder;
+
+ /**
+ * Array of arrays of executed queries, indexed by the temporary table names
+ * results were fed into.
+ *
+ * @var array
+ */
+ private $executedQueries = [];
+
+ /**
+ * Query mode copied from given query. Some submethods act differently when
+ * in Query::MODE_DEBUG.
+ *
+ * @var int
+ */
+ private $queryMode;
+
+ /**
+ * @var array
+ */
+ private $querySegmentList = [];
+
+ /**
+ * @param Database $connection
+ * @param TemporaryTableBuilder $temporaryTableBuilder
+ * @param HierarchyTempTableBuilder $hierarchyTempTableBuilder
+ */
+ public function __construct( $connection, TemporaryTableBuilder $temporaryTableBuilder, $hierarchyTempTableBuilder ) {
+ $this->connection = $connection;
+ $this->temporaryTableBuilder = $temporaryTableBuilder;
+ $this->hierarchyTempTableBuilder = $hierarchyTempTableBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getExecutedQueries() {
+ return $this->executedQueries;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param &$querySegmentList
+ */
+ public function setQuerySegmentList( &$querySegmentList ) {
+ $this->querySegmentList =& $querySegmentList;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param int $queryMode
+ */
+ public function setQueryMode( $queryMode ) {
+ $this->queryMode = $queryMode;
+ }
+
+ /**
+ * Process stored queries and change store accordingly. The query obj is modified
+ * so that it contains non-recursive description of a select to execute for getting
+ * the actual result.
+ *
+ * @param int $id
+ *
+ * @throws RuntimeException
+ */
+ public function process( $id ) {
+ $this->hierarchyTempTableBuilder->emptyHierarchyCache();
+ $this->executedQueries = [];
+
+ // Should never happen
+ if ( !isset( $this->querySegmentList[$id] ) ) {
+ throw new RuntimeException( "$id doesn't exist" );
+ }
+
+ $this->segment( $this->querySegmentList[$id] );
+ }
+
+ private function segment( QuerySegment &$query ) {
+ switch ( $query->type ) {
+ case QuerySegment::Q_TABLE: // .
+ $this->table( $query );
+ break;
+ case QuerySegment::Q_CONJUNCTION:
+ $this->conjunction( $query );
+ break;
+ case QuerySegment::Q_DISJUNCTION:
+ $this->disjunction( $query );
+ break;
+ case QuerySegment::Q_PROP_HIERARCHY:
+ case QuerySegment::Q_CLASS_HIERARCHY: // make a saturated hierarchy
+ $this->hierarchy( $query );
+ break;
+ case QuerySegment::Q_VALUE:
+ break; // nothing to do
+ }
+ }
+
+ /**
+ * Resolves normal queries with possible conjunctive subconditions
+ */
+ private function table( QuerySegment &$query ) {
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->segment( $subQuery );
+ $alias = $subQuery->alias;
+
+ if ( $subQuery->joinTable !== '' ) { // Join with jointable.joinfield
+ $op = $subQuery->not ? '!' : '';
+
+ $joinType = $subQuery->joinType ? $subQuery->joinType : 'INNER';
+ $t = $this->connection->tableName( $subQuery->joinTable ) . " AS $subQuery->alias";
+ // If the alias is the same as the table name and if there is a prefix, MediaWiki does not declare the unprefixed alias
+ $joinTable = $subQuery->joinTable === $subQuery->alias ? $this->connection->tableName( $subQuery->joinTable ) : $subQuery->joinTable;
+
+ if ( $subQuery->from ) {
+ $t = "($t $subQuery->from)";
+ $alias = 'nested' . $subQuery->alias;
+ $query->fromTables[$alias] = array_merge( (array)$subQuery->fromTables, [ $subQuery->alias => $joinTable ] );
+ $query->joinConditions = array_merge( (array)$query->joinConditions, (array)$subQuery->joinConditions );
+
+ } else {
+ $query->fromTables[$alias] = $joinTable;
+ }
+
+ $query->joinConditions[$alias] = [ $joinType . ' JOIN', "$joinField$op=" . $subQuery->joinfield ];
+
+ $this->fromTables[$subQuery->alias] = $joinTable;
+
+ ksort( $this->fromTables );
+ $this->joinConditions[$subQuery->alias] = [ $joinType . ' JOIN', "$joinField$op=" . $subQuery->joinfield ];
+
+ $query->from .= " $joinType JOIN $t ON $joinField$op=" . $subQuery->joinfield;
+ if ( $joinType === 'LEFT' ) {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->joinfield . ' IS NULL)';
+ }
+
+ } elseif ( $subQuery->joinfield !== '' ) { // Require joinfield as "value" via WHERE.
+ $condition = '';
+
+ if ( $subQuery->null === true ) {
+ $condition .= ( $condition ? ' OR ' : '' ) . "$joinField IS NULL";
+ } else {
+ foreach ( $subQuery->joinfield as $value ) {
+ $op = $subQuery->not ? '!' : '';
+ $condition .= ( $condition ? ' OR ' : '' ) . "$joinField$op=" . $this->connection->addQuotes( $value );
+ }
+ }
+
+ if ( count( $subQuery->joinfield ) > 1 ) {
+ $condition = "($condition)";
+ }
+
+ $query->where .= ( ( $query->where === '' || $subQuery->where === null ) ? '' : ' AND ' ) . $condition;
+ $query->from .= $subQuery->from;
+ $query->fromTables = array_merge( (array)$query->fromTables, (array)$subQuery->fromTables );
+ $query->joinConditions = array_merge( (array)$query->joinConditions, (array)$subQuery->joinConditions );
+ } else { // interpret empty joinfields as impossible condition (empty result)
+ $query->joinfield = ''; // make whole query false
+ $query->joinTable = '';
+ $query->where = '';
+ $query->from = '';
+ $query->fromTables = [];
+ $query->joinConditions = [];
+ break;
+ }
+
+ if ( $subQuery->where !== '' && $subQuery->where !== null ) {
+ if ( $subQuery->joinType === 'LEFT' || $subQuery->joinType == 'LEFT OUTER' ) {
+ $query->from .= ' AND (' . $subQuery->where . ')';
+ $query->joinConditions[$alias][1] .= ' AND (' . $subQuery->where . ')';
+ } else {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->where . ')';
+ }
+ }
+ }
+
+ $query->components = [];
+ }
+
+ private function conjunction( QuerySegment &$query ) {
+ reset( $query->components );
+ $key = false;
+
+ // Pick one subquery as anchor point ...
+ foreach ( $query->components as $qkey => $qid ) {
+ $key = $qkey;
+
+ if ( $this->querySegmentList[$qkey]->joinTable !== '' ) {
+ break;
+ }
+ }
+
+ $result = $this->querySegmentList[$key];
+ unset( $query->components[$key] );
+
+ // Execute it first (may change jointable and joinfield, e.g. when
+ // making temporary tables)
+ $this->segment( $result );
+
+ // ... and append to this query the remaining queries.
+ foreach ( $query->components as $qid => $joinfield ) {
+ $result->components[$qid] = $result->joinfield;
+ }
+
+ // Second execute, now incorporating remaining conditions.
+ $this->segment( $result );
+
+ $query = $result;
+ }
+
+ private function disjunction( QuerySegment &$query ) {
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->temporaryTableBuilder->create( $this->connection->tableName( $query->alias ) );
+ }
+
+ $this->executedQueries[$query->alias] = [];
+
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->segment( $subQuery );
+ $sql = '';
+
+ if ( $subQuery->joinTable !== '' ) {
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' .
+ $this->connection->tableName( $query->alias ) .
+ " SELECT DISTINCT $subQuery->joinfield FROM " . $this->connection->tableName( $subQuery->joinTable ) .
+ " AS $subQuery->alias $subQuery->from" . ( $subQuery->where ? " WHERE $subQuery->where" : '' );
+ } elseif ( $subQuery->joinfield !== '' ) {
+ // NOTE: this works only for single "unconditional" values without further
+ // WHERE or FROM. The execution must take care of not creating any others.
+ $values = '';
+
+ // This produces an error on postgres with
+ // pg_query(): Query failed: ERROR: duplicate key value violates
+ // unique constraint "sunittest_t3_pkey" DETAIL: Key (id)=(274) already exists.
+
+ foreach ( $subQuery->joinfield as $value ) {
+ $values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ }
+
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' . $this->connection->tableName( $query->alias ) . " (id) VALUES $values";
+ } // else: // interpret empty joinfields as impossible condition (empty result), ignore
+
+ if ( $sql ) {
+ $this->executedQueries[$query->alias][] = $sql;
+
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->connection->query(
+ $sql,
+ __METHOD__,
+ ISQLPlatform::QUERY_CHANGE_ROWS
+ );
+ }
+ }
+ }
+
+ $query->type = QuerySegment::Q_TABLE;
+ $query->where = '';
+ $query->components = [];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+ $query->sortfields = []; // Make sure we got no sortfields.
+
+ // TODO: currently this eliminates sortkeys, possibly keep them (needs
+ // different temp table format though, maybe not such a good thing to do)
+ }
+
+ /**
+ * Find subproperties or subcategories. This may require iterative computation,
+ * and temporary tables are used in many cases.
+ *
+ * @param QuerySegment &$query
+ */
+ private function hierarchy( QuerySegment &$query ) {
+ switch ( $query->type ) {
+ case QuerySegment::Q_PROP_HIERARCHY:
+ $type = 'property';
+ break;
+ case QuerySegment::Q_CLASS_HIERARCHY:
+ $type = 'class';
+ break;
+ }
+
+ [ $smwtable, $depth ] = $this->hierarchyTempTableBuilder->getTableDefinitionByType(
+ $type
+ );
+
+ // An individual depth was annotated as part of the query
+ if ( $query->depth !== null ) {
+ $depth = $query->depth;
+ }
+
+ if ( $depth <= 0 ) { // treat as value, no recursion
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $values = '';
+ $valuecond = '';
+
+ foreach ( $query->joinfield as $value ) {
+ $values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ $valuecond .= ( $valuecond ? ' OR ' : '' ) . 'o_id=' . $this->connection->addQuotes( $value );
+ }
+
+ // Try to safe time (SELECT is cheaper than creating/dropping 3 temp tables):
+ $res = $this->connection->select(
+ $smwtable,
+ 's_id',
+ $valuecond,
+ __METHOD__,
+ [ 'LIMIT' => 1 ]
+ );
+
+ if ( !$res->fetchObject() ) { // no subobjects, we are done!
+ $res->free();
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $res->free();
+ $tablename = $this->connection->tableName( $query->alias );
+ $this->executedQueries[$query->alias] = [
+ "Recursively computed hierarchy for element(s) $values.",
+ "SELECT s_id FROM $smwtable WHERE $valuecond LIMIT 1"
+ ];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+
+ $this->hierarchyTempTableBuilder->fillTempTable(
+ $type,
+ $tablename,
+ $values,
+ $depth
+ );
+ }
+
+ /**
+ * After querying, make sure no temporary database tables are left.
+ *
+ * @todo I might be better to keep the tables and possibly reuse them later
+ * on. Being temporary, the tables will vanish with the session anyway.
+ */
+ public function cleanUp() {
+ foreach ( $this->executedQueries as $table => $log ) {
+ $this->temporaryTableBuilder->drop( $this->connection->tableName( $table ) );
+ }
+ }
+}
diff --git a/formats/datatables/SearchPanes.php b/formats/datatables/SearchPanes.php
index 9c8b47c85..4cdfba65b 100644
--- a/formats/datatables/SearchPanes.php
+++ b/formats/datatables/SearchPanes.php
@@ -17,15 +17,18 @@
use SMW\DIWikiPage;
use SMW\Query\PrintRequest;
use SMW\Services\ServicesFactory as ApplicationFactory;
+use SMW\SQLStore\QueryEngine\HierarchyTempTableBuilder;
use SMW\SQLStore\QueryEngine\QuerySegment;
use SMW\SQLStore\QueryEngineFactory;
use SMW\SQLStore\SQLStore;
use SMW\SQLStore\TableBuilder\FieldType;
-use SMWDataItem as DataItem;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
use SMWQueryProcessor;
use SRF\DataTables;
class SearchPanes {
+ /** @var DataTables */
+ private $datatables;
private array $searchPanesLog = [];
@@ -33,9 +36,58 @@ class SearchPanes {
private $connection;
- public function __construct(
- private DataTables $datatables
- ) {
+ public function __construct( DataTables $datatables ) {
+ $this->datatables = $datatables;
+ }
+
+ private function newTemporaryTableBuilder() {
+ $temporaryTableBuilder = new TemporaryTableBuilder(
+ $this->datatables->store->getConnection( 'mw.db.queryengine' )
+ );
+
+ $temporaryTableBuilder->setAutoCommitFlag(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgQTemporaryTablesAutoCommitMode' )
+ );
+
+ return $temporaryTableBuilder;
+ }
+
+ /**
+ * @see SMW\SQLStore\QueryEngineFactory
+ * @return QuerySegmentListProcessor
+ */
+ public function newQuerySegmentListProcessor() {
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $connection = $this->datatables->store->getConnection( 'mw.db.queryengine' );
+ $temporaryTableBuilder = $this->newTemporaryTableBuilder();
+
+ $hierarchyTempTableBuilder = new HierarchyTempTableBuilder(
+ $connection,
+ $temporaryTableBuilder
+ );
+
+ $hierarchyTempTableBuilder->setTableDefinitions(
+ [
+ 'property' => [
+ 'table' => $this->datatables->store->findPropertyTableID( new DIProperty( '_SUBP' ) ),
+ 'depth' => $settings->get( 'smwgQSubpropertyDepth' )
+ ],
+ 'class' => [
+ 'table' => $this->datatables->store->findPropertyTableID( new DIProperty( '_SUBC' ) ),
+ 'depth' => $settings->get( 'smwgQSubcategoryDepth' )
+ ]
+
+ ]
+ );
+
+ $querySegmentListProcessor = new QuerySegmentListProcessor(
+ $connection,
+ $temporaryTableBuilder,
+ $hierarchyTempTableBuilder
+ );
+
+ return $querySegmentListProcessor;
}
public function getSearchPanes( array $printRequests, array $searchPanesOptions ): array {
@@ -117,7 +169,8 @@ private function getPanesOptions(
QuerySegment::$qnum = 0;
$querySegmentList = $conditionBuilder->getQuerySegmentList();
- $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor();
+ // $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor();
+ $querySegmentListProcessor = $this->newQuerySegmentListProcessor();
$querySegmentListProcessor->setQuerySegmentList( $querySegmentList );
@@ -126,30 +179,46 @@ private function getPanesOptions(
$qobj = $querySegmentList[$rootid];
+ $tables = $querySegmentListProcessor->fromTables;
+ $joins = $querySegmentListProcessor->joinConditions;
+
+ $tables[$qobj->alias] = $qobj->joinTable;
+
+ $conds = $qobj->where;
+
$property = new DIProperty( DIProperty::newFromUserLabel( $printRequest->getCanonicalLabel() ) );
$propTypeid = $property->findPropertyValueType();
if ( $isCategory ) {
-
// data-length without the GROUP BY clause
- $sql_options = [ 'LIMIT' => 1 ];
+ $sql_options_ = [ 'LIMIT' => 1 ];
+
+ $tables_ = $tables;
+ $tables_['insts'] = 'smw_fpt_inst';
+ $fields_ = [ 'count' => 'COUNT(*)' ];
+ $conds_ = $conds;
+ $joins_ = $joins;
+ $joins_['insts'] = [ 'JOIN', [ "$qobj->alias.smw_id = insts.s_id" ] ];
- $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,
+ $res = $this->connection->select(
+ $tables_,
+ $fields_,
+ $conds_,
__METHOD__,
- $sql_options
+ $sql_options_,
+ $joins_
);
+ $row = $res->fetchRow();
+ $dataLength = (int)( $row['count'] ?? 0 );
+
if ( !$dataLength ) {
return [];
}
$groupBy = "i.smw_id";
$orderBy = "count DESC, $groupBy ASC";
- $sql_options = [
+ $sql_options_ = [
'GROUP BY' => $groupBy,
// $this->query->getOption( 'count' ),
'LIMIT' => $dataLength,
@@ -167,15 +236,22 @@ private function getPanesOptions(
HAVING COUNT(i.smw_id) >= 1 ORDER BY COUNT(i.smw_id) DESC
*/
+ $tables_ = $tables;
+ $tables_['insts'] = 'smw_fpt_inst';
+ $tables_['i'] = SQLStore::ID_TABLE;
+ $joins_ = $joins;
+ $joins_['insts'] = [ 'JOIN', [ "$qobj->alias.smw_id = insts.s_id" ] ];
+ $joins_['i'] = [ 'JOIN', [ 'i.smw_id = insts.o_id' ] ];
+ $conds_ = $conds;
+ $fields_ = "COUNT($groupBy) AS count, i.smw_id, i.smw_title, i.smw_namespace, i.smw_iw, i.smw_sort, i.smw_subobject";
+
$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,
+ $tables_,
+ $fields_,
+ $conds_,
__METHOD__,
- $sql_options
+ $sql_options_,
+ $joins_
);
$isIdField = true;
@@ -203,25 +279,34 @@ private function getPanesOptions(
}
// data-length without the GROUP BY clause
- $sql_options = [ 'LIMIT' => 1 ];
+ $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,
+ $tables_ = $tables;
+ $fields_ = [ 'count' => 'COUNT(*)' ];
+ $conds_ = $conds;
+ $joins_ = $joins;
+
+ $res = $this->connection->select(
+ $tables_,
+ $fields_,
+ $conds_,
__METHOD__,
- $sql_options
+ $sql_options_,
+ $joins_
);
+ $row = $res->fetchRow();
+ $dataLength = (int)( $row['count'] ?? 0 );
+
if ( !$dataLength ) {
return [];
}
- [ $diType, $isIdField, $fields, $groupBy, $orderBy ] = $this->fetchValuesByGroup( $property, $p_alias, $propTypeid );
+ [ $diType, $isIdField, $fields_, $groupBy, $orderBy ] = $this->fetchValuesByGroup( $property, $p_alias, $propTypeid );
/*
---GENERATED FROM DATATABLES
@@ -231,7 +316,7 @@ private function getPanesOptions(
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
*/
- $sql_options = [
+ $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,
@@ -243,18 +328,26 @@ private function getPanesOptions(
];
// @see QueryEngine
+ $tables_ = $tables;
+ $joins_ = $joins;
+ $conds_ = $conds;
+
+ if ( $isIdField ) {
+ $tables_['i'] = SQLStore::ID_TABLE;
+ $joins_['i'] = [ 'JOIN', "$p_alias.o_id = i.smw_id" ];
+ $conds_ .= !empty( $conds_ ) ? ' AND' : '';
+ $conds_ .= ' i.smw_iw != ' . $this->connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED );
+ $conds_ .= ' AND i.smw_iw != ' . $this->connection->addQuotes( SMW_SQL3_SMWDELETEIW );
+ }
+
$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 ) ),
+ $tables_,
+ $fields_,
+ $conds_,
__METHOD__,
- $sql_options
+ $sql_options_,
+ $joins_
);
-
}
// verify uniqueRatio
@@ -307,7 +400,6 @@ private function getPanesOptions(
$isSubject = false;
$groups = [];
foreach ( $res as $row ) {
-
if ( $isIdField ) {
$dbKeys = [
$row->smw_title,
@@ -352,9 +444,7 @@ private function getPanesOptions(
$dataValue->setOutputFormat( $outputFormat );
}
-/*
-
-
+ /*
// @see DIBlobHandler
// $isKeyword = $dataItem->getOption( 'is.keyword' );
@@ -484,7 +574,6 @@ private function getPanesOptions(
if ( $uniqueRatio > $threshold ) {
return [];
}
-
}
arsort( $groups, SORT_NUMERIC );
@@ -619,7 +708,8 @@ private function searchPanesMainlabel( PrintRequest $printRequest, array $search
QuerySegment::$qnum = 0;
$querySegmentList = $conditionBuilder->getQuerySegmentList();
- $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor();
+ // $querySegmentListProcessor = $this->queryEngineFactory->newQuerySegmentListProcessor();
+ $querySegmentListProcessor = $this->newQuerySegmentListProcessor();
$querySegmentListProcessor->setQuerySegmentList( $querySegmentList );
@@ -628,7 +718,14 @@ private function searchPanesMainlabel( PrintRequest $printRequest, array $search
$qobj = $querySegmentList[$rootid];
- $sql_options = [
+ $tables = $querySegmentListProcessor->fromTables;
+ $joins = $querySegmentListProcessor->joinConditions;
+
+ $tables[$qobj->alias] = $qobj->joinTable;
+
+ $conds = $qobj->where;
+
+ $sql_options_ = [
// *** should we set a limit here ?
// it makes sense to show the pane for
// mainlabel only when page titles are grouped
@@ -639,21 +736,29 @@ private function searchPanesMainlabel( PrintRequest $printRequest, array $search
// Selecting those is required in standard SQL (but MySQL does not require it).
$sortfields = implode( ',', $qobj->sortfields );
- $sortfields = $sortfields ? ',' . $sortfields : '';
+ // $sortfields = $sortfields ? ',' . $sortfields : '';
// @see QueryEngine
+ $tables_ = $tables;
+ $fields_ = [];
+ $fields_['id'] = "$qobj->alias.smw_id";
+ $fields_['t'] = "$qobj->alias.smw_title";
+ $fields_['ns'] = "$qobj->alias.smw_namespace";
+ $fields_['iw'] = "$qobj->alias.smw_iw";
+ $fields_['so'] = "$qobj->alias.smw_subobject";
+ $fields_['sortkey'] = "$qobj->alias.smw_sortkey";
+ $fields_[] = $sortfields;
+
+ $conds_ = $conds;
+ $joins_ = $joins;
+
$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,
+ $tables_,
+ $fields_,
+ $conds_,
__METHOD__,
- $sql_options
+ $sql_options_,
+ $joins_
);
$diHandler = $this->datatables->store->getDataItemHandlerForDIType(
diff --git a/formats/filtered/package.json b/formats/filtered/package.json
index 052ac757a..c44e80c3e 100644
--- a/formats/filtered/package.json
+++ b/formats/filtered/package.json
@@ -1,6 +1,6 @@
{
"name": "filtered",
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "Displays SMW query results in switchable views and offers client-side (JavaScript based) filtering",
"main": "ext.srf.filtered.js",
"scripts": {
@@ -9,15 +9,17 @@
},
"author": "Stephan Gambke",
"license": "GPL-2.0-or-later",
- "dependencies": {},
+ "dependencies": {
+ },
"devDependencies": {
- "@types/fullcalendar": "2.7.42",
+ "@fullcalendar/core": "^6.1.15",
+ "@fullcalendar/daygrid": "^6.1.15",
"@types/ion.rangeslider": "^2.0.29",
"@types/jquery": "^2.0.51",
"@types/jqueryui": "^1.12.5",
- "@types/leaflet": "~1.0.60",
- "@types/leaflet-providers": "^1.1.0",
- "@types/leaflet.markercluster": "1.0.0",
+ "@types/leaflet": "^1.9.16",
+ "@types/leaflet-providers": "^1.2.4",
+ "@types/leaflet.markercluster": "^1.5.5",
"@types/qunit": "^1.16.31",
"@types/select2": "^4.0.47",
"browserify": "^16.2.3",
@@ -28,14 +30,14 @@
"gulp-sourcemaps": "^2.4.1",
"gulp-uglify": "^2.1.2",
"ion-rangeslider": "^2.2.0",
- "leaflet": "^1.3.4",
- "leaflet-providers": "^1.4.0",
- "leaflet.markercluster": "~1.2",
+ "leaflet": "^1.9.4",
+ "leaflet-providers": "^2.0.0",
+ "leaflet.markercluster": "^1.5.3",
"nouislider": "^10.1.0",
"select2": "4.0.3",
"tsify": "^3.0.1",
- "typescript": "^3.1.3",
"vinyl-buffer": "^1.0.0",
- "vinyl-source-stream": "^2.0.0"
+ "vinyl-source-stream": "^2.0.0",
+ "typescript": "^5.7.3"
}
}
diff --git a/formats/filtered/resources/css/ext.srf.filtered.leaflet.css b/formats/filtered/resources/css/ext.srf.filtered.leaflet.css
index 99786d4c4..fd50fb27e 100644
--- a/formats/filtered/resources/css/ext.srf.filtered.leaflet.css
+++ b/formats/filtered/resources/css/ext.srf.filtered.leaflet.css
@@ -25,6 +25,10 @@
user-select: none;
-webkit-user-drag: none;
}
+/* Prevents IE11 from highlighting tiles in blue */
+.leaflet-tile::selection {
+ background: transparent;
+}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
@@ -41,7 +45,10 @@
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
-.leaflet-container .leaflet-overlay-pane svg,
+.leaflet-container .leaflet-overlay-pane svg {
+ max-width: none !important;
+ max-height: none !important;
+ }
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
@@ -49,8 +56,15 @@
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
+ width: auto;
+ padding: 0;
}
+.leaflet-container img.leaflet-tile {
+ /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
+ mix-blend-mode: plus-lighter;
+}
+
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
@@ -162,9 +176,6 @@
/* zoom and fade animations */
-.leaflet-fade-anim .leaflet-tile {
- will-change: opacity;
- }
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
@@ -179,9 +190,10 @@
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
-.leaflet-zoom-anim .leaflet-zoom-animated {
+svg.leaflet-zoom-animated {
will-change: transform;
- }
+}
+
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
@@ -237,7 +249,8 @@
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
-.leaflet-pane > svg path.leaflet-interactive {
+.leaflet-pane > svg path.leaflet-interactive,
+svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
@@ -246,14 +259,11 @@
.leaflet-container {
background: #ddd;
- outline: 0;
+ outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
-.leaflet-container a.leaflet-active {
- outline: 2px solid orange;
- }
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
@@ -262,7 +272,10 @@
/* general typography */
.leaflet-container {
- font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
+ font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
+ font-size: 12px;
+ font-size: 0.75rem;
+ line-height: 1.5;
}
@@ -272,8 +285,7 @@
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
-.leaflet-bar a,
-.leaflet-bar a:hover {
+.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
@@ -290,7 +302,8 @@
background-repeat: no-repeat;
display: block;
}
-.leaflet-bar a:hover {
+.leaflet-bar a:hover,
+.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
@@ -380,6 +393,8 @@
}
.leaflet-control-layers label {
display: block;
+ font-size: 13px;
+ font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
@@ -388,7 +403,7 @@
}
/* Default icon URLs */
-.leaflet-default-icon-path {
+.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
@@ -397,23 +412,27 @@
.leaflet-container .leaflet-control-attribution {
background: #fff;
- background: rgba(255, 255, 255, 0.7);
+ background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
+ line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
-.leaflet-control-attribution a:hover {
+.leaflet-control-attribution a:hover,
+.leaflet-control-attribution a:focus {
text-decoration: underline;
}
-.leaflet-container .leaflet-control-attribution,
-.leaflet-container .leaflet-control-scale {
- font-size: 11px;
+.leaflet-attribution-flag {
+ display: inline !important;
+ vertical-align: baseline !important;
+ width: 1em;
+ height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
@@ -426,14 +445,11 @@
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
- font-size: 11px;
white-space: nowrap;
- overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
-
- background: #fff;
- background: rgba(255, 255, 255, 0.5);
+ background: rgba(255, 255, 255, 0.8);
+ text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
@@ -469,17 +485,22 @@
border-radius: 12px;
}
.leaflet-popup-content {
- margin: 13px 19px;
- line-height: 1.4;
+ margin: 13px 24px 13px 20px;
+ line-height: 1.3;
+ font-size: 13px;
+ font-size: 1.08333em;
+ min-height: 1px;
}
.leaflet-popup-content p {
- margin: 18px 0;
+ margin: 17px 0;
+ margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
+ margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
@@ -490,6 +511,7 @@
padding: 1px;
margin: -10px auto 0;
+ pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
@@ -506,28 +528,25 @@
position: absolute;
top: 0;
right: 0;
- padding: 4px 4px 0 0;
border: none;
text-align: center;
- width: 18px;
- height: 14px;
- font: 16px/14px Tahoma, Verdana, sans-serif;
- color: #c3c3c3;
+ width: 24px;
+ height: 24px;
+ font: 16px/24px Tahoma, Verdana, sans-serif;
+ color: #757575;
text-decoration: none;
- font-weight: bold;
background: transparent;
}
-.leaflet-container a.leaflet-popup-close-button:hover {
- color: #999;
+.leaflet-container a.leaflet-popup-close-button:hover,
+.leaflet-container a.leaflet-popup-close-button:focus {
+ color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
- zoom: 1;
+ -ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
@@ -536,9 +555,6 @@
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
-.leaflet-oldie .leaflet-popup-tip-container {
- margin-top: -1px;
- }
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
@@ -573,7 +589,7 @@
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
-.leaflet-tooltip.leaflet-clickable {
+.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
@@ -633,79 +649,89 @@
margin-left: -12px;
border-right-color: #fff;
}
-
-.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
- -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
- -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
- -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
- transition: transform 0.3s ease-out, opacity 0.3s ease-in;
-}
-
-.leaflet-cluster-spider-leg {
- /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
- -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
- -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
- -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
- transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
-}
-
-.marker-cluster-small {
- background-color: rgba(181, 226, 140, 0.6);
- }
-.marker-cluster-small div {
- background-color: rgba(110, 204, 57, 0.6);
- }
-.marker-cluster-medium {
- background-color: rgba(241, 211, 87, 0.6);
- }
-.marker-cluster-medium div {
- background-color: rgba(240, 194, 12, 0.6);
- }
-
-.marker-cluster-large {
- background-color: rgba(253, 156, 115, 0.6);
- }
-.marker-cluster-large div {
- background-color: rgba(241, 128, 23, 0.6);
- }
-
- /* IE 6-8 fallback colors */
-.leaflet-oldie .marker-cluster-small {
- background-color: rgb(181, 226, 140);
- }
-.leaflet-oldie .marker-cluster-small div {
- background-color: rgb(110, 204, 57);
- }
-
-.leaflet-oldie .marker-cluster-medium {
- background-color: rgb(241, 211, 87);
- }
-.leaflet-oldie .marker-cluster-medium div {
- background-color: rgb(240, 194, 12);
- }
-
-.leaflet-oldie .marker-cluster-large {
- background-color: rgb(253, 156, 115);
- }
-.leaflet-oldie .marker-cluster-large div {
- background-color: rgb(241, 128, 23);
-}
-
-.marker-cluster {
- background-clip: padding-box;
- border-radius: 20px;
- }
-.marker-cluster div {
- width: 30px;
- height: 30px;
- margin-left: 5px;
- margin-top: 5px;
+/* Printing */
- text-align: center;
- border-radius: 15px;
- font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
+@media print {
+ /* Prevent printers from removing background-images of controls. */
+ .leaflet-control {
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
}
-.marker-cluster span {
- line-height: 30px;
+
+.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
+ -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
+ -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
+ -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
+ transition: transform 0.3s ease-out, opacity 0.3s ease-in;
+}
+
+.leaflet-cluster-spider-leg {
+ /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
+ -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
+ -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
+ -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
+ transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
+}
+
+.marker-cluster-small {
+ background-color: rgba(181, 226, 140, 0.6);
+ }
+.marker-cluster-small div {
+ background-color: rgba(110, 204, 57, 0.6);
+ }
+
+.marker-cluster-medium {
+ background-color: rgba(241, 211, 87, 0.6);
+ }
+.marker-cluster-medium div {
+ background-color: rgba(240, 194, 12, 0.6);
+ }
+
+.marker-cluster-large {
+ background-color: rgba(253, 156, 115, 0.6);
+ }
+.marker-cluster-large div {
+ background-color: rgba(241, 128, 23, 0.6);
+ }
+
+ /* IE 6-8 fallback colors */
+.leaflet-oldie .marker-cluster-small {
+ background-color: rgb(181, 226, 140);
+ }
+.leaflet-oldie .marker-cluster-small div {
+ background-color: rgb(110, 204, 57);
+ }
+
+.leaflet-oldie .marker-cluster-medium {
+ background-color: rgb(241, 211, 87);
+ }
+.leaflet-oldie .marker-cluster-medium div {
+ background-color: rgb(240, 194, 12);
+ }
+
+.leaflet-oldie .marker-cluster-large {
+ background-color: rgb(253, 156, 115);
+ }
+.leaflet-oldie .marker-cluster-large div {
+ background-color: rgb(241, 128, 23);
+}
+
+.marker-cluster {
+ background-clip: padding-box;
+ border-radius: 20px;
+ }
+.marker-cluster div {
+ width: 30px;
+ height: 30px;
+ margin-left: 5px;
+ margin-top: 5px;
+
+ text-align: center;
+ border-radius: 15px;
+ font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
+ }
+.marker-cluster span {
+ line-height: 30px;
}
\ No newline at end of file
diff --git a/formats/filtered/resources/js/ext.srf.filtered.js b/formats/filtered/resources/js/ext.srf.filtered.js
index 39fea7a01..bcadd45b6 100644
--- a/formats/filtered/resources/js/ext.srf.filtered.js
+++ b/formats/filtered/resources/js/ext.srf.filtered.js
@@ -1,10 +1,29578 @@
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 ? explicitRawLocales[0].code : 'en';
+ let allRawLocales = globalLocales.concat(explicitRawLocales);
+ let rawLocaleMap = {
+ en: RAW_EN_LOCALE,
+ };
+ for (let rawLocale of allRawLocales) {
+ rawLocaleMap[rawLocale.code] = rawLocale;
+ }
+ return {
+ map: rawLocaleMap,
+ defaultCode,
+ };
+}
+function buildLocale(inputSingular, available) {
+ if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
+ return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
+ }
+ return queryLocale(inputSingular, available);
+}
+function queryLocale(codeArg, available) {
+ let codes = [].concat(codeArg || []); // will convert to array
+ let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
+ return parseLocale(codeArg, codes, raw);
+}
+function queryRawLocale(codes, available) {
+ for (let i = 0; i < codes.length; i += 1) {
+ let parts = codes[i].toLocaleLowerCase().split('-');
+ for (let j = parts.length; j > 0; j -= 1) {
+ let simpleId = parts.slice(0, j).join('-');
+ if (available[simpleId]) {
+ return available[simpleId];
+ }
+ }
+ }
+ return null;
+}
+function parseLocale(codeArg, codes, raw) {
+ let merged = internalCommon.mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']);
+ delete merged.code; // don't want this part of the options
+ let { week } = merged;
+ delete merged.week;
+ return {
+ codeArg,
+ codes,
+ week,
+ simpleNumberFormat: new Intl.NumberFormat(codeArg),
+ options: merged,
+ };
+}
+
+// TODO: easier way to add new hooks? need to update a million things
+function createPlugin(input) {
+ return {
+ id: internalCommon.guid(),
+ name: input.name,
+ premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
+ deps: input.deps || [],
+ reducers: input.reducers || [],
+ isLoadingFuncs: input.isLoadingFuncs || [],
+ contextInit: [].concat(input.contextInit || []),
+ eventRefiners: input.eventRefiners || {},
+ eventDefMemberAdders: input.eventDefMemberAdders || [],
+ eventSourceRefiners: input.eventSourceRefiners || {},
+ isDraggableTransformers: input.isDraggableTransformers || [],
+ eventDragMutationMassagers: input.eventDragMutationMassagers || [],
+ eventDefMutationAppliers: input.eventDefMutationAppliers || [],
+ dateSelectionTransformers: input.dateSelectionTransformers || [],
+ datePointTransforms: input.datePointTransforms || [],
+ dateSpanTransforms: input.dateSpanTransforms || [],
+ views: input.views || {},
+ viewPropsTransformers: input.viewPropsTransformers || [],
+ isPropsValid: input.isPropsValid || null,
+ externalDefTransforms: input.externalDefTransforms || [],
+ viewContainerAppends: input.viewContainerAppends || [],
+ eventDropTransformers: input.eventDropTransformers || [],
+ componentInteractions: input.componentInteractions || [],
+ calendarInteractions: input.calendarInteractions || [],
+ themeClasses: input.themeClasses || {},
+ eventSourceDefs: input.eventSourceDefs || [],
+ cmdFormatter: input.cmdFormatter,
+ recurringTypes: input.recurringTypes || [],
+ namedTimeZonedImpl: input.namedTimeZonedImpl,
+ initialView: input.initialView || '',
+ elementDraggingImpl: input.elementDraggingImpl,
+ optionChangeHandlers: input.optionChangeHandlers || {},
+ scrollGridImpl: input.scrollGridImpl || null,
+ listenerRefiners: input.listenerRefiners || {},
+ optionRefiners: input.optionRefiners || {},
+ propSetHandlers: input.propSetHandlers || {},
+ };
+}
+function buildPluginHooks(pluginDefs, globalDefs) {
+ let currentPluginIds = {};
+ let hooks = {
+ premiumReleaseDate: undefined,
+ reducers: [],
+ isLoadingFuncs: [],
+ contextInit: [],
+ eventRefiners: {},
+ eventDefMemberAdders: [],
+ eventSourceRefiners: {},
+ isDraggableTransformers: [],
+ eventDragMutationMassagers: [],
+ eventDefMutationAppliers: [],
+ dateSelectionTransformers: [],
+ datePointTransforms: [],
+ dateSpanTransforms: [],
+ views: {},
+ viewPropsTransformers: [],
+ isPropsValid: null,
+ externalDefTransforms: [],
+ viewContainerAppends: [],
+ eventDropTransformers: [],
+ componentInteractions: [],
+ calendarInteractions: [],
+ themeClasses: {},
+ eventSourceDefs: [],
+ cmdFormatter: null,
+ recurringTypes: [],
+ namedTimeZonedImpl: null,
+ initialView: '',
+ elementDraggingImpl: null,
+ optionChangeHandlers: {},
+ scrollGridImpl: null,
+ listenerRefiners: {},
+ optionRefiners: {},
+ propSetHandlers: {},
+ };
+ function addDefs(defs) {
+ for (let def of defs) {
+ const pluginName = def.name;
+ const currentId = currentPluginIds[pluginName];
+ if (currentId === undefined) {
+ currentPluginIds[pluginName] = def.id;
+ addDefs(def.deps);
+ hooks = combineHooks(hooks, def);
+ }
+ else if (currentId !== def.id) {
+ // different ID than the one already added
+ console.warn(`Duplicate plugin '${pluginName}'`);
+ }
+ }
+ }
+ if (pluginDefs) {
+ addDefs(pluginDefs);
+ }
+ addDefs(globalDefs);
+ return hooks;
+}
+function buildBuildPluginHooks() {
+ let currentOverrideDefs = [];
+ let currentGlobalDefs = [];
+ let currentHooks;
+ return (overrideDefs, globalDefs) => {
+ if (!currentHooks || !internalCommon.isArraysEqual(overrideDefs, currentOverrideDefs) || !internalCommon.isArraysEqual(globalDefs, currentGlobalDefs)) {
+ currentHooks = buildPluginHooks(overrideDefs, globalDefs);
+ }
+ currentOverrideDefs = overrideDefs;
+ currentGlobalDefs = globalDefs;
+ return currentHooks;
+ };
+}
+function combineHooks(hooks0, hooks1) {
+ return {
+ premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate),
+ reducers: hooks0.reducers.concat(hooks1.reducers),
+ isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs),
+ contextInit: hooks0.contextInit.concat(hooks1.contextInit),
+ eventRefiners: Object.assign(Object.assign({}, hooks0.eventRefiners), hooks1.eventRefiners),
+ eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
+ eventSourceRefiners: Object.assign(Object.assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners),
+ isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
+ eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
+ eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
+ dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
+ datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
+ dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
+ views: Object.assign(Object.assign({}, hooks0.views), hooks1.views),
+ viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
+ isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
+ externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
+ viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
+ eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
+ calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
+ componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
+ themeClasses: Object.assign(Object.assign({}, hooks0.themeClasses), hooks1.themeClasses),
+ eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
+ cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
+ recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
+ namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
+ initialView: hooks0.initialView || hooks1.initialView,
+ elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
+ optionChangeHandlers: Object.assign(Object.assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers),
+ scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
+ listenerRefiners: Object.assign(Object.assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners),
+ optionRefiners: Object.assign(Object.assign({}, hooks0.optionRefiners), hooks1.optionRefiners),
+ propSetHandlers: Object.assign(Object.assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers),
+ };
+}
+function compareOptionalDates(date0, date1) {
+ if (date0 === undefined) {
+ return date1;
+ }
+ if (date1 === undefined) {
+ return date0;
+ }
+ return new Date(Math.max(date0.valueOf(), date1.valueOf()));
+}
+
+class StandardTheme extends internalCommon.Theme {
+}
+StandardTheme.prototype.classes = {
+ root: 'fc-theme-standard',
+ tableCellShaded: 'fc-cell-shaded',
+ buttonGroup: 'fc-button-group',
+ button: 'fc-button fc-button-primary',
+ buttonActive: 'fc-button-active',
+};
+StandardTheme.prototype.baseIconClass = 'fc-icon';
+StandardTheme.prototype.iconClasses = {
+ close: 'fc-icon-x',
+ prev: 'fc-icon-chevron-left',
+ next: 'fc-icon-chevron-right',
+ prevYear: 'fc-icon-chevrons-left',
+ nextYear: 'fc-icon-chevrons-right',
+};
+StandardTheme.prototype.rtlIconClasses = {
+ prev: 'fc-icon-chevron-right',
+ next: 'fc-icon-chevron-left',
+ prevYear: 'fc-icon-chevrons-right',
+ nextYear: 'fc-icon-chevrons-left',
+};
+StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly
+StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
+StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
+
+function compileViewDefs(defaultConfigs, overrideConfigs) {
+ let hash = {};
+ let viewType;
+ for (viewType in defaultConfigs) {
+ ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
+ }
+ for (viewType in overrideConfigs) {
+ ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
+ }
+ return hash;
+}
+function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
+ if (hash[viewType]) {
+ return hash[viewType];
+ }
+ let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
+ if (viewDef) {
+ hash[viewType] = viewDef;
+ }
+ return viewDef;
+}
+function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
+ let defaultConfig = defaultConfigs[viewType];
+ let overrideConfig = overrideConfigs[viewType];
+ let queryProp = (name) => ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
+ ((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null));
+ let theComponent = queryProp('component');
+ let superType = queryProp('superType');
+ let superDef = null;
+ if (superType) {
+ if (superType === viewType) {
+ throw new Error('Can\'t have a custom view type that references itself');
+ }
+ superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
+ }
+ if (!theComponent && superDef) {
+ theComponent = superDef.component;
+ }
+ if (!theComponent) {
+ return null; // don't throw a warning, might be settings for a single-unit view
+ }
+ return {
+ type: viewType,
+ component: theComponent,
+ defaults: Object.assign(Object.assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})),
+ overrides: Object.assign(Object.assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})),
+ };
+}
+
+function parseViewConfigs(inputs) {
+ return internalCommon.mapHash(inputs, parseViewConfig);
+}
+function parseViewConfig(input) {
+ let rawOptions = typeof input === 'function' ?
+ { component: input } :
+ input;
+ let { component } = rawOptions;
+ if (rawOptions.content) {
+ // TODO: remove content/classNames/didMount/etc from options?
+ component = createViewHookComponent(rawOptions);
+ }
+ else if (component && !(component.prototype instanceof internalCommon.BaseComponent)) {
+ // WHY?: people were using `component` property for `content`
+ // TODO: converge on one setting name
+ component = createViewHookComponent(Object.assign(Object.assign({}, rawOptions), { content: component }));
+ }
+ return {
+ superType: rawOptions.type,
+ component: component,
+ rawOptions, // includes type and component too :(
+ };
+}
+function createViewHookComponent(options) {
+ return (viewProps) => (preact.createElement(internalCommon.ViewContextType.Consumer, null, (context) => (preact.createElement(internalCommon.ContentContainer, { elTag: "div", elClasses: internalCommon.buildViewClassNames(context.viewSpec), renderProps: Object.assign(Object.assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }), generatorName: undefined, customGenerator: options.content, classNameGenerator: options.classNames, didMount: options.didMount, willUnmount: options.willUnmount }))));
+}
+
+function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
+ let defaultConfigs = parseViewConfigs(defaultInputs);
+ let overrideConfigs = parseViewConfigs(optionOverrides.views);
+ let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
+ return internalCommon.mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults));
+}
+function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
+ let durationInput = viewDef.overrides.duration ||
+ viewDef.defaults.duration ||
+ dynamicOptionOverrides.duration ||
+ optionOverrides.duration;
+ let duration = null;
+ let durationUnit = '';
+ let singleUnit = '';
+ let singleUnitOverrides = {};
+ if (durationInput) {
+ duration = createDurationCached(durationInput);
+ if (duration) { // valid?
+ let denom = internalCommon.greatestDurationDenominator(duration);
+ durationUnit = denom.unit;
+ if (denom.value === 1) {
+ singleUnit = durationUnit;
+ singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {};
+ }
+ }
+ }
+ let queryButtonText = (optionsSubset) => {
+ let buttonTextMap = optionsSubset.buttonText || {};
+ let buttonTextKey = viewDef.defaults.buttonTextKey;
+ if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
+ return buttonTextMap[buttonTextKey];
+ }
+ if (buttonTextMap[viewDef.type] != null) {
+ return buttonTextMap[viewDef.type];
+ }
+ if (buttonTextMap[singleUnit] != null) {
+ return buttonTextMap[singleUnit];
+ }
+ return null;
+ };
+ let queryButtonTitle = (optionsSubset) => {
+ let buttonHints = optionsSubset.buttonHints || {};
+ let buttonKey = viewDef.defaults.buttonTextKey; // use same key as text
+ if (buttonKey != null && buttonHints[buttonKey] != null) {
+ return buttonHints[buttonKey];
+ }
+ if (buttonHints[viewDef.type] != null) {
+ return buttonHints[viewDef.type];
+ }
+ if (buttonHints[singleUnit] != null) {
+ return buttonHints[singleUnit];
+ }
+ return null;
+ };
+ return {
+ type: viewDef.type,
+ component: viewDef.component,
+ duration,
+ durationUnit,
+ singleUnit,
+ optionDefaults: viewDef.defaults,
+ optionOverrides: Object.assign(Object.assign({}, singleUnitOverrides), viewDef.overrides),
+ buttonTextOverride: queryButtonText(dynamicOptionOverrides) ||
+ queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence
+ viewDef.overrides.buttonText,
+ buttonTextDefault: queryButtonText(localeDefaults) ||
+ viewDef.defaults.buttonText ||
+ queryButtonText(internalCommon.BASE_OPTION_DEFAULTS) ||
+ viewDef.type,
+ // not DRY
+ buttonTitleOverride: queryButtonTitle(dynamicOptionOverrides) ||
+ queryButtonTitle(optionOverrides) ||
+ viewDef.overrides.buttonHint,
+ buttonTitleDefault: queryButtonTitle(localeDefaults) ||
+ viewDef.defaults.buttonHint ||
+ queryButtonTitle(internalCommon.BASE_OPTION_DEFAULTS),
+ // will eventually fall back to buttonText
+ };
+}
+// hack to get memoization working
+let durationInputMap = {};
+function createDurationCached(durationInput) {
+ let json = JSON.stringify(durationInput);
+ let res = durationInputMap[json];
+ if (res === undefined) {
+ res = internalCommon.createDuration(durationInput);
+ durationInputMap[json] = res;
+ }
+ return res;
+}
+
+function reduceViewType(viewType, action) {
+ switch (action.type) {
+ case 'CHANGE_VIEW_TYPE':
+ viewType = action.viewType;
+ }
+ return viewType;
+}
+
+function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) {
+ switch (action.type) {
+ case 'SET_OPTION':
+ return Object.assign(Object.assign({}, dynamicOptionOverrides), { [action.optionName]: action.rawOptionValue });
+ default:
+ return dynamicOptionOverrides;
+ }
+}
+
+function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) {
+ let dp;
+ switch (action.type) {
+ case 'CHANGE_VIEW_TYPE':
+ return dateProfileGenerator.build(action.dateMarker || currentDate);
+ case 'CHANGE_DATE':
+ return dateProfileGenerator.build(action.dateMarker);
+ case 'PREV':
+ dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate);
+ if (dp.isValid) {
+ return dp;
+ }
+ break;
+ case 'NEXT':
+ dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate);
+ if (dp.isValid) {
+ return dp;
+ }
+ break;
+ }
+ return currentDateProfile;
+}
+
+function initEventSources(calendarOptions, dateProfile, context) {
+ let activeRange = dateProfile ? dateProfile.activeRange : null;
+ return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context);
+}
+function reduceEventSources(eventSources, action, dateProfile, context) {
+ let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
+ switch (action.type) {
+ case 'ADD_EVENT_SOURCES': // already parsed
+ return addSources(eventSources, action.sources, activeRange, context);
+ case 'REMOVE_EVENT_SOURCE':
+ return removeSource(eventSources, action.sourceId);
+ case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
+ case 'NEXT':
+ case 'CHANGE_DATE':
+ case 'CHANGE_VIEW_TYPE':
+ if (dateProfile) {
+ return fetchDirtySources(eventSources, activeRange, context);
+ }
+ return eventSources;
+ case 'FETCH_EVENT_SOURCES':
+ return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type?
+ internalCommon.arrayToHash(action.sourceIds) :
+ excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context);
+ case 'RECEIVE_EVENTS':
+ case 'RECEIVE_EVENT_ERROR':
+ return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
+ case 'REMOVE_ALL_EVENT_SOURCES':
+ return {};
+ default:
+ return eventSources;
+ }
+}
+function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) {
+ let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
+ return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context);
+}
+function computeEventSourcesLoading(eventSources) {
+ for (let sourceId in eventSources) {
+ if (eventSources[sourceId].isFetching) {
+ return true;
+ }
+ }
+ return false;
+}
+function addSources(eventSourceHash, sources, fetchRange, context) {
+ let hash = {};
+ for (let source of sources) {
+ hash[source.sourceId] = source;
+ }
+ if (fetchRange) {
+ hash = fetchDirtySources(hash, fetchRange, context);
+ }
+ return Object.assign(Object.assign({}, eventSourceHash), hash);
+}
+function removeSource(eventSourceHash, sourceId) {
+ return internalCommon.filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId);
+}
+function fetchDirtySources(sourceHash, fetchRange, context) {
+ return fetchSourcesByIds(sourceHash, internalCommon.filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), fetchRange, false, context);
+}
+function isSourceDirty(eventSource, fetchRange, context) {
+ if (!doesSourceNeedRange(eventSource, context)) {
+ return !eventSource.latestFetchId;
+ }
+ return !context.options.lazyFetching ||
+ !eventSource.fetchRange ||
+ eventSource.isFetching || // always cancel outdated in-progress fetches
+ fetchRange.start < eventSource.fetchRange.start ||
+ fetchRange.end > eventSource.fetchRange.end;
+}
+function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) {
+ let nextSources = {};
+ for (let sourceId in prevSources) {
+ let source = prevSources[sourceId];
+ if (sourceIdHash[sourceId]) {
+ nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context);
+ }
+ else {
+ nextSources[sourceId] = source;
+ }
+ }
+ return nextSources;
+}
+function fetchSource(eventSource, fetchRange, isRefetch, context) {
+ let { options, calendarApi } = context;
+ let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId];
+ let fetchId = internalCommon.guid();
+ sourceDef.fetch({
+ eventSource,
+ range: fetchRange,
+ isRefetch,
+ context,
+ }, (res) => {
+ let { rawEvents } = res;
+ if (options.eventSourceSuccess) {
+ rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents;
+ }
+ if (eventSource.success) {
+ rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents;
+ }
+ context.dispatch({
+ type: 'RECEIVE_EVENTS',
+ sourceId: eventSource.sourceId,
+ fetchId,
+ fetchRange,
+ rawEvents,
+ });
+ }, (error) => {
+ let errorHandled = false;
+ if (options.eventSourceFailure) {
+ options.eventSourceFailure.call(calendarApi, error);
+ errorHandled = true;
+ }
+ if (eventSource.failure) {
+ eventSource.failure(error);
+ errorHandled = true;
+ }
+ if (!errorHandled) {
+ console.warn(error.message, error);
+ }
+ context.dispatch({
+ type: 'RECEIVE_EVENT_ERROR',
+ sourceId: eventSource.sourceId,
+ fetchId,
+ fetchRange,
+ error,
+ });
+ });
+ return Object.assign(Object.assign({}, eventSource), { isFetching: true, latestFetchId: fetchId });
+}
+function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
+ let eventSource = sourceHash[sourceId];
+ if (eventSource && // not already removed
+ fetchId === eventSource.latestFetchId) {
+ return Object.assign(Object.assign({}, sourceHash), { [sourceId]: Object.assign(Object.assign({}, eventSource), { isFetching: false, fetchRange }) });
+ }
+ return sourceHash;
+}
+function excludeStaticSources(eventSources, context) {
+ return internalCommon.filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context));
+}
+function parseInitialSources(rawOptions, context) {
+ let refiners = internalCommon.buildEventSourceRefiners(context);
+ let rawSources = [].concat(rawOptions.eventSources || []);
+ let sources = []; // parsed
+ if (rawOptions.initialEvents) {
+ rawSources.unshift(rawOptions.initialEvents);
+ }
+ if (rawOptions.events) {
+ rawSources.unshift(rawOptions.events);
+ }
+ for (let rawSource of rawSources) {
+ let source = internalCommon.parseEventSource(rawSource, context, refiners);
+ if (source) {
+ sources.push(source);
+ }
+ }
+ return sources;
+}
+function doesSourceNeedRange(eventSource, context) {
+ let defs = context.pluginHooks.eventSourceDefs;
+ return !defs[eventSource.sourceDefId].ignoreRange;
+}
+
+function reduceDateSelection(currentSelection, action) {
+ switch (action.type) {
+ case 'UNSELECT_DATES':
+ return null;
+ case 'SELECT_DATES':
+ return action.selection;
+ default:
+ return currentSelection;
+ }
+}
+
+function reduceSelectedEvent(currentInstanceId, action) {
+ switch (action.type) {
+ case 'UNSELECT_EVENT':
+ return '';
+ case 'SELECT_EVENT':
+ return action.eventInstanceId;
+ default:
+ return currentInstanceId;
+ }
+}
+
+function reduceEventDrag(currentDrag, action) {
+ let newDrag;
+ switch (action.type) {
+ case 'UNSET_EVENT_DRAG':
+ return null;
+ case 'SET_EVENT_DRAG':
+ newDrag = action.state;
+ return {
+ affectedEvents: newDrag.affectedEvents,
+ mutatedEvents: newDrag.mutatedEvents,
+ isEvent: newDrag.isEvent,
+ };
+ default:
+ return currentDrag;
+ }
+}
+
+function reduceEventResize(currentResize, action) {
+ let newResize;
+ switch (action.type) {
+ case 'UNSET_EVENT_RESIZE':
+ return null;
+ case 'SET_EVENT_RESIZE':
+ newResize = action.state;
+ return {
+ affectedEvents: newResize.affectedEvents,
+ mutatedEvents: newResize.mutatedEvents,
+ isEvent: newResize.isEvent,
+ };
+ default:
+ return currentResize;
+ }
+}
+
+function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
+ let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
+ let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
+ return { header, footer };
+}
+function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
+ let sectionWidgets = {};
+ let viewsWithButtons = [];
+ let hasTitle = false;
+ for (let sectionName in sectionStrHash) {
+ let sectionStr = sectionStrHash[sectionName];
+ let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi);
+ sectionWidgets[sectionName] = sectionRes.widgets;
+ viewsWithButtons.push(...sectionRes.viewsWithButtons);
+ hasTitle = hasTitle || sectionRes.hasTitle;
+ }
+ return { sectionWidgets, viewsWithButtons, hasTitle };
+}
+/*
+BAD: querying icons and text here. should be done at render time
+*/
+function parseSection(sectionStr, calendarOptions, // defaults+overrides, then refined
+calendarOptionOverrides, // overrides only!, unrefined :(
+theme, viewSpecs, calendarApi) {
+ let isRtl = calendarOptions.direction === 'rtl';
+ let calendarCustomButtons = calendarOptions.customButtons || {};
+ let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {};
+ let calendarButtonText = calendarOptions.buttonText || {};
+ let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {};
+ let calendarButtonHints = calendarOptions.buttonHints || {};
+ let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
+ let viewsWithButtons = [];
+ let hasTitle = false;
+ let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((buttonName) => {
+ if (buttonName === 'title') {
+ hasTitle = true;
+ return { buttonName };
+ }
+ let customButtonProps;
+ let viewSpec;
+ let buttonClick;
+ let buttonIcon; // only one of these will be set
+ let buttonText; // "
+ let buttonHint;
+ // ^ for the title="" attribute, for accessibility
+ if ((customButtonProps = calendarCustomButtons[buttonName])) {
+ buttonClick = (ev) => {
+ if (customButtonProps.click) {
+ customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context?
+ }
+ };
+ (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
+ (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
+ (buttonText = customButtonProps.text);
+ buttonHint = customButtonProps.hint || customButtonProps.text;
+ }
+ else if ((viewSpec = viewSpecs[buttonName])) {
+ viewsWithButtons.push(buttonName);
+ buttonClick = () => {
+ calendarApi.changeView(buttonName);
+ };
+ (buttonText = viewSpec.buttonTextOverride) ||
+ (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
+ (buttonText = viewSpec.buttonTextDefault);
+ let textFallback = viewSpec.buttonTextOverride ||
+ viewSpec.buttonTextDefault;
+ buttonHint = internalCommon.formatWithOrdinals(viewSpec.buttonTitleOverride ||
+ viewSpec.buttonTitleDefault ||
+ calendarOptions.viewHint, [textFallback, buttonName], // view-name = buttonName
+ textFallback);
+ }
+ else if (calendarApi[buttonName]) { // a calendarApi method
+ buttonClick = () => {
+ calendarApi[buttonName]();
+ };
+ (buttonText = calendarButtonTextOverrides[buttonName]) ||
+ (buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
+ (buttonText = calendarButtonText[buttonName]); // everything else is considered default
+ if (buttonName === 'prevYear' || buttonName === 'nextYear') {
+ let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next';
+ buttonHint = internalCommon.formatWithOrdinals(calendarButtonHintOverrides[prevOrNext] ||
+ calendarButtonHints[prevOrNext], [
+ calendarButtonText.year || 'year',
+ 'year',
+ ], calendarButtonText[buttonName]);
+ }
+ else {
+ buttonHint = (navUnit) => internalCommon.formatWithOrdinals(calendarButtonHintOverrides[buttonName] ||
+ calendarButtonHints[buttonName], [
+ calendarButtonText[navUnit] || navUnit,
+ navUnit,
+ ], calendarButtonText[buttonName]);
+ }
+ }
+ return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint };
+ })));
+ return { widgets, viewsWithButtons, hasTitle };
+}
+
+// always represents the current view. otherwise, it'd need to change value every time date changes
+class ViewImpl {
+ constructor(type, getCurrentData, dateEnv) {
+ this.type = type;
+ this.getCurrentData = getCurrentData;
+ this.dateEnv = dateEnv;
+ }
+ get calendar() {
+ return this.getCurrentData().calendarApi;
+ }
+ get title() {
+ return this.getCurrentData().viewTitle;
+ }
+ get activeStart() {
+ return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start);
+ }
+ get activeEnd() {
+ return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end);
+ }
+ get currentStart() {
+ return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start);
+ }
+ get currentEnd() {
+ return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end);
+ }
+ getOption(name) {
+ return this.getCurrentData().options[name]; // are the view-specific options
+ }
+}
+
+let eventSourceDef$2 = {
+ ignoreRange: true,
+ parseMeta(refined) {
+ if (Array.isArray(refined.events)) {
+ return refined.events;
+ }
+ return null;
+ },
+ fetch(arg, successCallback) {
+ successCallback({
+ rawEvents: arg.eventSource.meta,
+ });
+ },
+};
+const arrayEventSourcePlugin = createPlugin({
+ name: 'array-event-source',
+ eventSourceDefs: [eventSourceDef$2],
+});
+
+let eventSourceDef$1 = {
+ parseMeta(refined) {
+ if (typeof refined.events === 'function') {
+ return refined.events;
+ }
+ return null;
+ },
+ fetch(arg, successCallback, errorCallback) {
+ const { dateEnv } = arg.context;
+ const func = arg.eventSource.meta;
+ internalCommon.unpromisify(func.bind(null, internalCommon.buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback);
+ },
+};
+const funcEventSourcePlugin = createPlugin({
+ name: 'func-event-source',
+ eventSourceDefs: [eventSourceDef$1],
+});
+
+const JSON_FEED_EVENT_SOURCE_REFINERS = {
+ method: String,
+ extraParams: internalCommon.identity,
+ startParam: String,
+ endParam: String,
+ timeZoneParam: String,
+};
+
+let eventSourceDef = {
+ parseMeta(refined) {
+ if (refined.url && (refined.format === 'json' || !refined.format)) {
+ return {
+ url: refined.url,
+ format: 'json',
+ method: (refined.method || 'GET').toUpperCase(),
+ extraParams: refined.extraParams,
+ startParam: refined.startParam,
+ endParam: refined.endParam,
+ timeZoneParam: refined.timeZoneParam,
+ };
+ }
+ return null;
+ },
+ fetch(arg, successCallback, errorCallback) {
+ const { meta } = arg.eventSource;
+ const requestParams = buildRequestParams(meta, arg.range, arg.context);
+ internalCommon.requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => {
+ successCallback({ rawEvents, response });
+ }, errorCallback);
+ },
+};
+const jsonFeedEventSourcePlugin = createPlugin({
+ name: 'json-event-source',
+ eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS,
+ eventSourceDefs: [eventSourceDef],
+});
+function buildRequestParams(meta, range, context) {
+ let { dateEnv, options } = context;
+ let startParam;
+ let endParam;
+ let timeZoneParam;
+ let customRequestParams;
+ let params = {};
+ startParam = meta.startParam;
+ if (startParam == null) {
+ startParam = options.startParam;
+ }
+ endParam = meta.endParam;
+ if (endParam == null) {
+ endParam = options.endParam;
+ }
+ timeZoneParam = meta.timeZoneParam;
+ if (timeZoneParam == null) {
+ timeZoneParam = options.timeZoneParam;
+ }
+ // retrieve any outbound GET/POST data from the options
+ if (typeof meta.extraParams === 'function') {
+ // supplied as a function that returns a key/value object
+ customRequestParams = meta.extraParams();
+ }
+ else {
+ // probably supplied as a straight key/value object
+ customRequestParams = meta.extraParams || {};
+ }
+ Object.assign(params, customRequestParams);
+ params[startParam] = dateEnv.formatIso(range.start);
+ params[endParam] = dateEnv.formatIso(range.end);
+ if (dateEnv.timeZone !== 'local') {
+ params[timeZoneParam] = dateEnv.timeZone;
+ }
+ return params;
+}
+
+const SIMPLE_RECURRING_REFINERS = {
+ daysOfWeek: internalCommon.identity,
+ startTime: internalCommon.createDuration,
+ endTime: internalCommon.createDuration,
+ duration: internalCommon.createDuration,
+ startRecur: internalCommon.identity,
+ endRecur: internalCommon.identity,
+};
+
+let recurring = {
+ parse(refined, dateEnv) {
+ if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) {
+ let recurringData = {
+ daysOfWeek: refined.daysOfWeek || null,
+ startTime: refined.startTime || null,
+ endTime: refined.endTime || null,
+ startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null,
+ endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null,
+ };
+ let duration;
+ if (refined.duration) {
+ duration = refined.duration;
+ }
+ if (!duration && refined.startTime && refined.endTime) {
+ duration = internalCommon.subtractDurations(refined.endTime, refined.startTime);
+ }
+ return {
+ allDayGuess: Boolean(!refined.startTime && !refined.endTime),
+ duration,
+ typeData: recurringData, // doesn't need endTime anymore but oh well
+ };
+ }
+ return null;
+ },
+ expand(typeData, framingRange, dateEnv) {
+ let clippedFramingRange = internalCommon.intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
+ if (clippedFramingRange) {
+ return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv);
+ }
+ return [];
+ },
+};
+const simpleRecurringEventsPlugin = createPlugin({
+ name: 'simple-recurring-event',
+ recurringTypes: [recurring],
+ eventRefiners: SIMPLE_RECURRING_REFINERS,
+});
+function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) {
+ let dowHash = daysOfWeek ? internalCommon.arrayToHash(daysOfWeek) : null;
+ let dayMarker = internalCommon.startOfDay(framingRange.start);
+ let endMarker = framingRange.end;
+ let instanceStarts = [];
+ while (dayMarker < endMarker) {
+ let instanceStart;
+ // if everyday, or this particular day-of-week
+ if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
+ if (startTime) {
+ instanceStart = dateEnv.add(dayMarker, startTime);
+ }
+ else {
+ instanceStart = dayMarker;
+ }
+ instanceStarts.push(instanceStart);
+ }
+ dayMarker = internalCommon.addDays(dayMarker, 1);
+ }
+ return instanceStarts;
+}
+
+const changeHandlerPlugin = createPlugin({
+ name: 'change-handler',
+ optionChangeHandlers: {
+ events(events, context) {
+ handleEventSources([events], context);
+ },
+ eventSources: handleEventSources,
+ },
+});
+/*
+BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out
+*/
+function handleEventSources(inputs, context) {
+ let unfoundSources = internalCommon.hashValuesToArray(context.getCurrentData().eventSources);
+ if (unfoundSources.length === 1 &&
+ inputs.length === 1 &&
+ Array.isArray(unfoundSources[0]._raw) &&
+ Array.isArray(inputs[0])) {
+ context.dispatch({
+ type: 'RESET_RAW_EVENTS',
+ sourceId: unfoundSources[0].sourceId,
+ rawEvents: inputs[0],
+ });
+ return;
+ }
+ let newInputs = [];
+ for (let input of inputs) {
+ let inputFound = false;
+ for (let i = 0; i < unfoundSources.length; i += 1) {
+ if (unfoundSources[i]._raw === input) {
+ unfoundSources.splice(i, 1); // delete
+ inputFound = true;
+ break;
+ }
+ }
+ if (!inputFound) {
+ newInputs.push(input);
+ }
+ }
+ for (let unfoundSource of unfoundSources) {
+ context.dispatch({
+ type: 'REMOVE_EVENT_SOURCE',
+ sourceId: unfoundSource.sourceId,
+ });
+ }
+ for (let newInput of newInputs) {
+ context.calendarApi.addEventSource(newInput);
+ }
+}
+
+function handleDateProfile(dateProfile, context) {
+ context.emitter.trigger('datesSet', Object.assign(Object.assign({}, internalCommon.buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi }));
+}
+
+function handleEventStore(eventStore, context) {
+ let { emitter } = context;
+ if (emitter.hasHandlers('eventsSet')) {
+ emitter.trigger('eventsSet', internalCommon.buildEventApis(eventStore, context));
+ }
+}
+
+/*
+this array is exposed on the root namespace so that UMD plugins can add to it.
+see the rollup-bundles script.
+*/
+const globalPlugins = [
+ arrayEventSourcePlugin,
+ funcEventSourcePlugin,
+ jsonFeedEventSourcePlugin,
+ simpleRecurringEventsPlugin,
+ changeHandlerPlugin,
+ createPlugin({
+ name: 'misc',
+ isLoadingFuncs: [
+ (state) => computeEventSourcesLoading(state.eventSources),
+ ],
+ propSetHandlers: {
+ dateProfile: handleDateProfile,
+ eventStore: handleEventStore,
+ },
+ }),
+];
+
+class TaskRunner {
+ constructor(runTaskOption, drainedOption) {
+ this.runTaskOption = runTaskOption;
+ this.drainedOption = drainedOption;
+ this.queue = [];
+ this.delayedRunner = new internalCommon.DelayedRunner(this.drain.bind(this));
+ }
+ request(task, delay) {
+ this.queue.push(task);
+ this.delayedRunner.request(delay);
+ }
+ pause(scope) {
+ this.delayedRunner.pause(scope);
+ }
+ resume(scope, force) {
+ this.delayedRunner.resume(scope, force);
+ }
+ drain() {
+ let { queue } = this;
+ while (queue.length) {
+ let completedTasks = [];
+ let task;
+ while ((task = queue.shift())) {
+ this.runTask(task);
+ completedTasks.push(task);
+ }
+ this.drained(completedTasks);
+ } // keep going, in case new tasks were added in the drained handler
+ }
+ runTask(task) {
+ if (this.runTaskOption) {
+ this.runTaskOption(task);
+ }
+ }
+ drained(completedTasks) {
+ if (this.drainedOption) {
+ this.drainedOption(completedTasks);
+ }
+ }
+}
+
+// Computes what the title at the top of the calendarApi should be for this view
+function buildTitle(dateProfile, viewOptions, dateEnv) {
+ let range;
+ // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
+ if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
+ range = dateProfile.currentRange;
+ }
+ else { // for day units or smaller, use the actual day range
+ range = dateProfile.activeRange;
+ }
+ return dateEnv.formatRange(range.start, range.end, internalCommon.createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), {
+ isEndExclusive: dateProfile.isRangeAllDay,
+ defaultSeparator: viewOptions.titleRangeSeparator,
+ });
+}
+// Generates the format string that should be used to generate the title for the current date range.
+// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
+function buildTitleFormat(dateProfile) {
+ let { currentRangeUnit } = dateProfile;
+ if (currentRangeUnit === 'year') {
+ return { year: 'numeric' };
+ }
+ if (currentRangeUnit === 'month') {
+ return { year: 'numeric', month: 'long' }; // like "September 2014"
+ }
+ let days = internalCommon.diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
+ if (days !== null && days > 1) {
+ // multi-day range. shorter, like "Sep 9 - 10 2014"
+ return { year: 'numeric', month: 'short', day: 'numeric' };
+ }
+ // one day. longer, like "September 9 2014"
+ return { year: 'numeric', month: 'long', day: 'numeric' };
+}
+
+// in future refactor, do the redux-style function(state=initial) for initial-state
+// also, whatever is happening in constructor, have it happen in action queue too
+class CalendarDataManager {
+ constructor(props) {
+ this.computeCurrentViewData = internalCommon.memoize(this._computeCurrentViewData);
+ this.organizeRawLocales = internalCommon.memoize(organizeRawLocales);
+ this.buildLocale = internalCommon.memoize(buildLocale);
+ this.buildPluginHooks = buildBuildPluginHooks();
+ this.buildDateEnv = internalCommon.memoize(buildDateEnv$1);
+ this.buildTheme = internalCommon.memoize(buildTheme);
+ this.parseToolbars = internalCommon.memoize(parseToolbars);
+ this.buildViewSpecs = internalCommon.memoize(buildViewSpecs);
+ this.buildDateProfileGenerator = internalCommon.memoizeObjArg(buildDateProfileGenerator);
+ this.buildViewApi = internalCommon.memoize(buildViewApi);
+ this.buildViewUiProps = internalCommon.memoizeObjArg(buildViewUiProps);
+ this.buildEventUiBySource = internalCommon.memoize(buildEventUiBySource, internalCommon.isPropsEqual);
+ this.buildEventUiBases = internalCommon.memoize(buildEventUiBases);
+ this.parseContextBusinessHours = internalCommon.memoizeObjArg(parseContextBusinessHours);
+ this.buildTitle = internalCommon.memoize(buildTitle);
+ this.emitter = new internalCommon.Emitter();
+ this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this));
+ this.currentCalendarOptionsInput = {};
+ this.currentCalendarOptionsRefined = {};
+ this.currentViewOptionsInput = {};
+ this.currentViewOptionsRefined = {};
+ this.currentCalendarOptionsRefiners = {};
+ this.optionsForRefining = [];
+ this.optionsForHandling = [];
+ this.getCurrentData = () => this.data;
+ this.dispatch = (action) => {
+ this.actionRunner.request(action); // protects against recursive calls to _handleAction
+ };
+ this.props = props;
+ this.actionRunner.pause();
+ let dynamicOptionOverrides = {};
+ let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
+ let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView;
+ let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
+ // wire things up
+ // TODO: not DRY
+ props.calendarApi.currentDataManager = this;
+ this.emitter.setThisContext(props.calendarApi);
+ this.emitter.setOptions(currentViewData.options);
+ let currentDate = internalCommon.getInitialDate(optionsData.calendarOptions, optionsData.dateEnv);
+ let dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
+ if (!internalCommon.rangeContainsMarker(dateProfile.activeRange, currentDate)) {
+ currentDate = dateProfile.currentRange.start;
+ }
+ let calendarContext = {
+ dateEnv: optionsData.dateEnv,
+ options: optionsData.calendarOptions,
+ pluginHooks: optionsData.pluginHooks,
+ calendarApi: props.calendarApi,
+ dispatch: this.dispatch,
+ emitter: this.emitter,
+ getCurrentData: this.getCurrentData,
+ };
+ // needs to be after setThisContext
+ for (let callback of optionsData.pluginHooks.contextInit) {
+ callback(calendarContext);
+ }
+ // NOT DRY
+ let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext);
+ let initialState = {
+ dynamicOptionOverrides,
+ currentViewType,
+ currentDate,
+ dateProfile,
+ businessHours: this.parseContextBusinessHours(calendarContext),
+ eventSources,
+ eventUiBases: {},
+ eventStore: internalCommon.createEmptyEventStore(),
+ renderableEventStore: internalCommon.createEmptyEventStore(),
+ dateSelection: null,
+ eventSelection: '',
+ eventDrag: null,
+ eventResize: null,
+ selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig,
+ };
+ let contextAndState = Object.assign(Object.assign({}, calendarContext), initialState);
+ for (let reducer of optionsData.pluginHooks.reducers) {
+ Object.assign(initialState, reducer(null, null, contextAndState));
+ }
+ if (computeIsLoading(initialState, calendarContext)) {
+ this.emitter.trigger('loading', true); // NOT DRY
+ }
+ this.state = initialState;
+ this.updateData();
+ this.actionRunner.resume();
+ }
+ resetOptions(optionOverrides, changedOptionNames) {
+ let { props } = this;
+ if (changedOptionNames === undefined) {
+ props.optionOverrides = optionOverrides;
+ }
+ else {
+ props.optionOverrides = Object.assign(Object.assign({}, (props.optionOverrides || {})), optionOverrides);
+ this.optionsForRefining.push(...changedOptionNames);
+ }
+ if (changedOptionNames === undefined || changedOptionNames.length) {
+ this.actionRunner.request({
+ type: 'NOTHING',
+ });
+ }
+ }
+ _handleAction(action) {
+ let { props, state, emitter } = this;
+ let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action);
+ let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
+ let currentViewType = reduceViewType(state.currentViewType, action);
+ let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
+ // wire things up
+ // TODO: not DRY
+ props.calendarApi.currentDataManager = this;
+ emitter.setThisContext(props.calendarApi);
+ emitter.setOptions(currentViewData.options);
+ let calendarContext = {
+ dateEnv: optionsData.dateEnv,
+ options: optionsData.calendarOptions,
+ pluginHooks: optionsData.pluginHooks,
+ calendarApi: props.calendarApi,
+ dispatch: this.dispatch,
+ emitter,
+ getCurrentData: this.getCurrentData,
+ };
+ let { currentDate, dateProfile } = state;
+ if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack
+ dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
+ }
+ currentDate = internalCommon.reduceCurrentDate(currentDate, action);
+ dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator);
+ if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator
+ action.type === 'NEXT' || // "
+ !internalCommon.rangeContainsMarker(dateProfile.currentRange, currentDate)) {
+ currentDate = dateProfile.currentRange.start;
+ }
+ let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext);
+ let eventStore = internalCommon.reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext);
+ let isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading
+ let renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ?
+ (state.renderableEventStore || eventStore) : // try from previous state
+ eventStore;
+ let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext); // will memoize obj
+ let eventUiBySource = this.buildEventUiBySource(eventSources);
+ let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
+ let newState = {
+ dynamicOptionOverrides,
+ currentViewType,
+ currentDate,
+ dateProfile,
+ eventSources,
+ eventStore,
+ renderableEventStore,
+ selectionConfig,
+ eventUiBases,
+ businessHours: this.parseContextBusinessHours(calendarContext),
+ dateSelection: reduceDateSelection(state.dateSelection, action),
+ eventSelection: reduceSelectedEvent(state.eventSelection, action),
+ eventDrag: reduceEventDrag(state.eventDrag, action),
+ eventResize: reduceEventResize(state.eventResize, action),
+ };
+ let contextAndState = Object.assign(Object.assign({}, calendarContext), newState);
+ for (let reducer of optionsData.pluginHooks.reducers) {
+ Object.assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value
+ }
+ let wasLoading = computeIsLoading(state, calendarContext);
+ let isLoading = computeIsLoading(newState, calendarContext);
+ // TODO: use propSetHandlers in plugin system
+ if (!wasLoading && isLoading) {
+ emitter.trigger('loading', true);
+ }
+ else if (wasLoading && !isLoading) {
+ emitter.trigger('loading', false);
+ }
+ this.state = newState;
+ if (props.onAction) {
+ props.onAction(action);
+ }
+ }
+ updateData() {
+ let { props, state } = this;
+ let oldData = this.data;
+ let optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi);
+ let currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides);
+ let data = this.data = Object.assign(Object.assign(Object.assign({ viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state);
+ let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
+ let oldCalendarOptions = oldData && oldData.calendarOptions;
+ let newCalendarOptions = optionsData.calendarOptions;
+ if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) {
+ if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
+ // hack
+ state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data);
+ state.eventStore = data.eventStore = internalCommon.rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv);
+ state.renderableEventStore = data.renderableEventStore = internalCommon.rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv);
+ }
+ for (let optionName in changeHandlers) {
+ if (this.optionsForHandling.indexOf(optionName) !== -1 ||
+ oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
+ changeHandlers[optionName](newCalendarOptions[optionName], data);
+ }
+ }
+ }
+ this.optionsForHandling = [];
+ if (props.onData) {
+ props.onData(data);
+ }
+ }
+ computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi) {
+ // TODO: blacklist options that are handled by optionChangeHandlers
+ if (!this.optionsForRefining.length &&
+ optionOverrides === this.stableOptionOverrides &&
+ dynamicOptionOverrides === this.stableDynamicOptionOverrides) {
+ return this.stableCalendarOptionsData;
+ }
+ let { refinedOptions, pluginHooks, localeDefaults, availableLocaleData, extra, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
+ warnUnknownOptions(extra);
+ let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator);
+ let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults);
+ let theme = this.buildTheme(refinedOptions, pluginHooks);
+ let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi);
+ return this.stableCalendarOptionsData = {
+ calendarOptions: refinedOptions,
+ pluginHooks,
+ dateEnv,
+ viewSpecs,
+ theme,
+ toolbarConfig,
+ localeDefaults,
+ availableRawLocales: availableLocaleData.map,
+ };
+ }
+ // always called from behind a memoizer
+ processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
+ let { locales, locale } = internalCommon.mergeRawOptions([
+ internalCommon.BASE_OPTION_DEFAULTS,
+ optionOverrides,
+ dynamicOptionOverrides,
+ ]);
+ let availableLocaleData = this.organizeRawLocales(locales);
+ let availableRawLocales = availableLocaleData.map;
+ let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options;
+ let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins);
+ let refiners = this.currentCalendarOptionsRefiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, internalCommon.BASE_OPTION_REFINERS), internalCommon.CALENDAR_LISTENER_REFINERS), internalCommon.CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
+ let extra = {};
+ let raw = internalCommon.mergeRawOptions([
+ internalCommon.BASE_OPTION_DEFAULTS,
+ localeDefaults,
+ optionOverrides,
+ dynamicOptionOverrides,
+ ]);
+ let refined = {};
+ let currentRaw = this.currentCalendarOptionsInput;
+ let currentRefined = this.currentCalendarOptionsRefined;
+ let anyChanges = false;
+ for (let optionName in raw) {
+ if (this.optionsForRefining.indexOf(optionName) === -1 && (raw[optionName] === currentRaw[optionName] || (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] &&
+ (optionName in currentRaw) &&
+ internalCommon.COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])))) {
+ refined[optionName] = currentRefined[optionName];
+ }
+ else if (refiners[optionName]) {
+ refined[optionName] = refiners[optionName](raw[optionName]);
+ anyChanges = true;
+ }
+ else {
+ extra[optionName] = currentRaw[optionName];
+ }
+ }
+ if (anyChanges) {
+ this.currentCalendarOptionsInput = raw;
+ this.currentCalendarOptionsRefined = refined;
+ this.stableOptionOverrides = optionOverrides;
+ this.stableDynamicOptionOverrides = dynamicOptionOverrides;
+ }
+ this.optionsForHandling.push(...this.optionsForRefining);
+ this.optionsForRefining = [];
+ return {
+ rawOptions: this.currentCalendarOptionsInput,
+ refinedOptions: this.currentCalendarOptionsRefined,
+ pluginHooks,
+ availableLocaleData,
+ localeDefaults,
+ extra,
+ };
+ }
+ _computeCurrentViewData(viewType, optionsData, optionOverrides, dynamicOptionOverrides) {
+ let viewSpec = optionsData.viewSpecs[viewType];
+ if (!viewSpec) {
+ throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`);
+ }
+ let { refinedOptions, extra } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
+ warnUnknownOptions(extra);
+ let dateProfileGenerator = this.buildDateProfileGenerator({
+ dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
+ duration: viewSpec.duration,
+ durationUnit: viewSpec.durationUnit,
+ usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
+ dateEnv: optionsData.dateEnv,
+ calendarApi: this.props.calendarApi,
+ slotMinTime: refinedOptions.slotMinTime,
+ slotMaxTime: refinedOptions.slotMaxTime,
+ showNonCurrentDates: refinedOptions.showNonCurrentDates,
+ dayCount: refinedOptions.dayCount,
+ dateAlignment: refinedOptions.dateAlignment,
+ dateIncrement: refinedOptions.dateIncrement,
+ hiddenDays: refinedOptions.hiddenDays,
+ weekends: refinedOptions.weekends,
+ nowInput: refinedOptions.now,
+ validRangeInput: refinedOptions.validRange,
+ visibleRangeInput: refinedOptions.visibleRange,
+ fixedWeekCount: refinedOptions.fixedWeekCount,
+ });
+ let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv);
+ return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi };
+ }
+ processRawViewOptions(viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) {
+ let raw = internalCommon.mergeRawOptions([
+ internalCommon.BASE_OPTION_DEFAULTS,
+ viewSpec.optionDefaults,
+ localeDefaults,
+ optionOverrides,
+ viewSpec.optionOverrides,
+ dynamicOptionOverrides,
+ ]);
+ let refiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, internalCommon.BASE_OPTION_REFINERS), internalCommon.CALENDAR_LISTENER_REFINERS), internalCommon.CALENDAR_OPTION_REFINERS), internalCommon.VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
+ let refined = {};
+ let currentRaw = this.currentViewOptionsInput;
+ let currentRefined = this.currentViewOptionsRefined;
+ let anyChanges = false;
+ let extra = {};
+ for (let optionName in raw) {
+ if (raw[optionName] === currentRaw[optionName] ||
+ (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] &&
+ internalCommon.COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName]))) {
+ refined[optionName] = currentRefined[optionName];
+ }
+ else {
+ if (raw[optionName] === this.currentCalendarOptionsInput[optionName] ||
+ (internalCommon.COMPLEX_OPTION_COMPARATORS[optionName] &&
+ internalCommon.COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName]))) {
+ if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop
+ refined[optionName] = this.currentCalendarOptionsRefined[optionName];
+ }
+ }
+ else if (refiners[optionName]) {
+ refined[optionName] = refiners[optionName](raw[optionName]);
+ }
+ else {
+ extra[optionName] = raw[optionName];
+ }
+ anyChanges = true;
+ }
+ }
+ if (anyChanges) {
+ this.currentViewOptionsInput = raw;
+ this.currentViewOptionsRefined = refined;
+ }
+ return {
+ rawOptions: this.currentViewOptionsInput,
+ refinedOptions: this.currentViewOptionsRefined,
+ extra,
+ };
+ }
+}
+function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) {
+ let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
+ return new internalCommon.DateEnv({
+ calendarSystem: 'gregory',
+ timeZone,
+ namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
+ locale,
+ weekNumberCalculation,
+ firstDay,
+ weekText,
+ cmdFormatter: pluginHooks.cmdFormatter,
+ defaultSeparator,
+ });
+}
+function buildTheme(options, pluginHooks) {
+ let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme;
+ return new ThemeClass(options);
+}
+function buildDateProfileGenerator(props) {
+ let DateProfileGeneratorClass = props.dateProfileGeneratorClass || internalCommon.DateProfileGenerator;
+ return new DateProfileGeneratorClass(props);
+}
+function buildViewApi(type, getCurrentData, dateEnv) {
+ return new ViewImpl(type, getCurrentData, dateEnv);
+}
+function buildEventUiBySource(eventSources) {
+ return internalCommon.mapHash(eventSources, (eventSource) => eventSource.ui);
+}
+function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
+ let eventUiBases = { '': eventUiSingleBase };
+ for (let defId in eventDefs) {
+ let def = eventDefs[defId];
+ if (def.sourceId && eventUiBySource[def.sourceId]) {
+ eventUiBases[defId] = eventUiBySource[def.sourceId];
+ }
+ }
+ return eventUiBases;
+}
+function buildViewUiProps(calendarContext) {
+ let { options } = calendarContext;
+ return {
+ eventUiSingleBase: internalCommon.createEventUi({
+ display: options.eventDisplay,
+ editable: options.editable,
+ startEditable: options.eventStartEditable,
+ durationEditable: options.eventDurationEditable,
+ constraint: options.eventConstraint,
+ overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
+ allow: options.eventAllow,
+ backgroundColor: options.eventBackgroundColor,
+ borderColor: options.eventBorderColor,
+ textColor: options.eventTextColor,
+ color: options.eventColor,
+ // classNames: options.eventClassNames // render hook will handle this
+ }, calendarContext),
+ selectionConfig: internalCommon.createEventUi({
+ constraint: options.selectConstraint,
+ overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined,
+ allow: options.selectAllow,
+ }, calendarContext),
+ };
+}
+function computeIsLoading(state, context) {
+ for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) {
+ if (isLoadingFunc(state)) {
+ return true;
+ }
+ }
+ return false;
+}
+function parseContextBusinessHours(calendarContext) {
+ return internalCommon.parseBusinessHours(calendarContext.options.businessHours, calendarContext);
+}
+function warnUnknownOptions(options, viewName) {
+ for (let optionName in options) {
+ console.warn(`Unknown option '${optionName}'` +
+ (viewName ? ` for view '${viewName}'` : ''));
+ }
+}
+
+class ToolbarSection extends internalCommon.BaseComponent {
+ render() {
+ let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
+ return preact.createElement('div', { className: 'fc-toolbar-chunk' }, ...children);
+ }
+ renderWidgetGroup(widgetGroup) {
+ let { props } = this;
+ let { theme } = this.context;
+ let children = [];
+ let isOnlyButtons = true;
+ for (let widget of widgetGroup) {
+ let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget;
+ if (buttonName === 'title') {
+ isOnlyButtons = false;
+ children.push(preact.createElement("h2", { className: "fc-toolbar-title", id: props.titleId }, props.title));
+ }
+ else {
+ let isPressed = buttonName === props.activeButton;
+ let isDisabled = (!props.isTodayEnabled && buttonName === 'today') ||
+ (!props.isPrevEnabled && buttonName === 'prev') ||
+ (!props.isNextEnabled && buttonName === 'next');
+ let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')];
+ if (isPressed) {
+ buttonClasses.push(theme.getClass('buttonActive'));
+ }
+ children.push(preact.createElement("button", { type: "button", title: typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint, disabled: isDisabled, "aria-pressed": isPressed, className: buttonClasses.join(' '), onClick: buttonClick }, buttonText || (buttonIcon ? preact.createElement("span", { className: buttonIcon, role: "img" }) : '')));
+ }
+ }
+ if (children.length > 1) {
+ let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || '';
+ return preact.createElement('div', { className: groupClassName }, ...children);
+ }
+ return children[0];
+ }
+}
+
+class Toolbar extends internalCommon.BaseComponent {
+ render() {
+ let { model, extraClassName } = this.props;
+ let forceLtr = false;
+ let startContent;
+ let endContent;
+ let sectionWidgets = model.sectionWidgets;
+ let centerContent = sectionWidgets.center;
+ if (sectionWidgets.left) {
+ forceLtr = true;
+ startContent = sectionWidgets.left;
+ }
+ else {
+ startContent = sectionWidgets.start;
+ }
+ if (sectionWidgets.right) {
+ forceLtr = true;
+ endContent = sectionWidgets.right;
+ }
+ else {
+ endContent = sectionWidgets.end;
+ }
+ let classNames = [
+ extraClassName || '',
+ 'fc-toolbar',
+ forceLtr ? 'fc-toolbar-ltr' : '',
+ ];
+ return (preact.createElement("div", { className: classNames.join(' ') },
+ this.renderSection('start', startContent || []),
+ this.renderSection('center', centerContent || []),
+ this.renderSection('end', endContent || [])));
+ }
+ renderSection(key, widgetGroups) {
+ let { props } = this;
+ return (preact.createElement(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, navUnit: props.navUnit, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled, titleId: props.titleId }));
+ }
+}
+
+class ViewHarness extends internalCommon.BaseComponent {
+ constructor() {
+ super(...arguments);
+ this.state = {
+ availableWidth: null,
+ };
+ this.handleEl = (el) => {
+ this.el = el;
+ internalCommon.setRef(this.props.elRef, el);
+ this.updateAvailableWidth();
+ };
+ this.handleResize = () => {
+ this.updateAvailableWidth();
+ };
+ }
+ render() {
+ let { props, state } = this;
+ let { aspectRatio } = props;
+ let classNames = [
+ 'fc-view-harness',
+ (aspectRatio || props.liquid || props.height)
+ ? 'fc-view-harness-active' // harness controls the height
+ : 'fc-view-harness-passive', // let the view do the height
+ ];
+ let height = '';
+ let paddingBottom = '';
+ if (aspectRatio) {
+ if (state.availableWidth !== null) {
+ height = state.availableWidth / aspectRatio;
+ }
+ else {
+ // while waiting to know availableWidth, we can't set height to *zero*
+ // because will cause lots of unnecessary scrollbars within scrollgrid.
+ // BETTER: don't start rendering ANYTHING yet until we know container width
+ // NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606)
+ paddingBottom = `${(1 / aspectRatio) * 100}%`;
+ }
+ }
+ else {
+ height = props.height || '';
+ }
+ return (preact.createElement("div", { "aria-labelledby": props.labeledById, ref: this.handleEl, className: classNames.join(' '), style: { height, paddingBottom } }, props.children));
+ }
+ componentDidMount() {
+ this.context.addResizeHandler(this.handleResize);
+ }
+ componentWillUnmount() {
+ this.context.removeResizeHandler(this.handleResize);
+ }
+ updateAvailableWidth() {
+ if (this.el && // needed. but why?
+ this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth
+ ) {
+ this.setState({ availableWidth: this.el.offsetWidth });
+ }
+ }
+}
+
+/*
+Detects when the user clicks on an event within a DateComponent
+*/
+class EventClicking extends internalCommon.Interaction {
+ constructor(settings) {
+ super(settings);
+ this.handleSegClick = (ev, segEl) => {
+ let { component } = this;
+ let { context } = component;
+ let seg = internalCommon.getElSeg(segEl);
+ if (seg && // might be the surrounding the more link
+ component.isValidSegDownEl(ev.target)) {
+ // our way to simulate a link click for elements that can't be
tags
+ // grab before trigger fired in case trigger trashes DOM thru rerendering
+ let hasUrlContainer = internalCommon.elementClosest(ev.target, '.fc-event-forced-url');
+ let url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : '';
+ context.emitter.trigger('eventClick', {
+ el: segEl,
+ event: new internalCommon.EventImpl(component.context, seg.eventRange.def, seg.eventRange.instance),
+ jsEvent: ev,
+ view: context.viewApi,
+ });
+ if (url && !ev.defaultPrevented) {
+ window.location.href = url;
+ }
+ }
+ };
+ this.destroy = internalCommon.listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events
+ this.handleSegClick);
+ }
+}
+
+/*
+Triggers events and adds/removes core classNames when the user's pointer
+enters/leaves event-elements of a component.
+*/
+class EventHovering extends internalCommon.Interaction {
+ constructor(settings) {
+ super(settings);
+ // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
+ this.handleEventElRemove = (el) => {
+ if (el === this.currentSegEl) {
+ this.handleSegLeave(null, this.currentSegEl);
+ }
+ };
+ this.handleSegEnter = (ev, segEl) => {
+ if (internalCommon.getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
+ this.currentSegEl = segEl;
+ this.triggerEvent('eventMouseEnter', ev, segEl);
+ }
+ };
+ this.handleSegLeave = (ev, segEl) => {
+ if (this.currentSegEl) {
+ this.currentSegEl = null;
+ this.triggerEvent('eventMouseLeave', ev, segEl);
+ }
+ };
+ this.removeHoverListeners = internalCommon.listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events
+ this.handleSegEnter, this.handleSegLeave);
+ }
+ destroy() {
+ this.removeHoverListeners();
+ }
+ triggerEvent(publicEvName, ev, segEl) {
+ let { component } = this;
+ let { context } = component;
+ let seg = internalCommon.getElSeg(segEl);
+ if (!ev || component.isValidSegDownEl(ev.target)) {
+ context.emitter.trigger(publicEvName, {
+ el: segEl,
+ event: new internalCommon.EventImpl(context, seg.eventRange.def, seg.eventRange.instance),
+ jsEvent: ev,
+ view: context.viewApi,
+ });
+ }
+ }
+}
+
+class CalendarContent extends internalCommon.PureComponent {
+ constructor() {
+ super(...arguments);
+ this.buildViewContext = internalCommon.memoize(internalCommon.buildViewContext);
+ this.buildViewPropTransformers = internalCommon.memoize(buildViewPropTransformers);
+ this.buildToolbarProps = internalCommon.memoize(buildToolbarProps);
+ this.headerRef = preact.createRef();
+ this.footerRef = preact.createRef();
+ this.interactionsStore = {};
+ // eslint-disable-next-line
+ this.state = {
+ viewLabelId: internalCommon.getUniqueDomId(),
+ };
+ // Component Registration
+ // -----------------------------------------------------------------------------------------------------------------
+ this.registerInteractiveComponent = (component, settingsInput) => {
+ let settings = internalCommon.parseInteractionSettings(component, settingsInput);
+ let DEFAULT_INTERACTIONS = [
+ EventClicking,
+ EventHovering,
+ ];
+ let interactionClasses = DEFAULT_INTERACTIONS.concat(this.props.pluginHooks.componentInteractions);
+ let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings));
+ this.interactionsStore[component.uid] = interactions;
+ internalCommon.interactionSettingsStore[component.uid] = settings;
+ };
+ this.unregisterInteractiveComponent = (component) => {
+ let listeners = this.interactionsStore[component.uid];
+ if (listeners) {
+ for (let listener of listeners) {
+ listener.destroy();
+ }
+ delete this.interactionsStore[component.uid];
+ }
+ delete internalCommon.interactionSettingsStore[component.uid];
+ };
+ // Resizing
+ // -----------------------------------------------------------------------------------------------------------------
+ this.resizeRunner = new internalCommon.DelayedRunner(() => {
+ this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ?
+ this.props.emitter.trigger('windowResize', { view: this.props.viewApi });
+ });
+ this.handleWindowResize = (ev) => {
+ let { options } = this.props;
+ if (options.handleWindowResize &&
+ ev.target === window // avoid jqui events
+ ) {
+ this.resizeRunner.request(options.windowResizeDelay);
+ }
+ };
+ }
+ /*
+ renders INSIDE of an outer div
+ */
+ render() {
+ let { props } = this;
+ let { toolbarConfig, options } = props;
+ let toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, internalCommon.getNow(props.options.now, props.dateEnv), // TODO: use NowTimer????
+ props.viewTitle);
+ let viewVGrow = false;
+ let viewHeight = '';
+ let viewAspectRatio;
+ if (props.isHeightAuto || props.forPrint) {
+ viewHeight = '';
+ }
+ else if (options.height != null) {
+ viewVGrow = true;
+ }
+ else if (options.contentHeight != null) {
+ viewHeight = options.contentHeight;
+ }
+ else {
+ viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall
+ }
+ let viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
+ let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle)
+ ? this.state.viewLabelId
+ : undefined;
+ return (preact.createElement(internalCommon.ViewContextType.Provider, { value: viewContext },
+ toolbarConfig.header && (preact.createElement(Toolbar, Object.assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.header, titleId: viewLabelId }, toolbarProps))),
+ preact.createElement(ViewHarness, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, labeledById: viewLabelId },
+ this.renderView(props),
+ this.buildAppendContent()),
+ toolbarConfig.footer && (preact.createElement(Toolbar, Object.assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footer, titleId: "" }, toolbarProps)))));
+ }
+ componentDidMount() {
+ let { props } = this;
+ this.calendarInteractions = props.pluginHooks.calendarInteractions
+ .map((CalendarInteractionClass) => new CalendarInteractionClass(props));
+ window.addEventListener('resize', this.handleWindowResize);
+ let { propSetHandlers } = props.pluginHooks;
+ for (let propName in propSetHandlers) {
+ propSetHandlers[propName](props[propName], props);
+ }
+ }
+ componentDidUpdate(prevProps) {
+ let { props } = this;
+ let { propSetHandlers } = props.pluginHooks;
+ for (let propName in propSetHandlers) {
+ if (props[propName] !== prevProps[propName]) {
+ propSetHandlers[propName](props[propName], props);
+ }
+ }
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleWindowResize);
+ this.resizeRunner.clear();
+ for (let interaction of this.calendarInteractions) {
+ interaction.destroy();
+ }
+ this.props.emitter.trigger('_unmount');
+ }
+ buildAppendContent() {
+ let { props } = this;
+ let children = props.pluginHooks.viewContainerAppends.map((buildAppendContent) => buildAppendContent(props));
+ return preact.createElement(preact.Fragment, {}, ...children);
+ }
+ renderView(props) {
+ let { pluginHooks } = props;
+ let { viewSpec } = props;
+ let viewProps = {
+ dateProfile: props.dateProfile,
+ businessHours: props.businessHours,
+ eventStore: props.renderableEventStore,
+ eventUiBases: props.eventUiBases,
+ dateSelection: props.dateSelection,
+ eventSelection: props.eventSelection,
+ eventDrag: props.eventDrag,
+ eventResize: props.eventResize,
+ isHeightAuto: props.isHeightAuto,
+ forPrint: props.forPrint,
+ };
+ let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
+ for (let transformer of transformers) {
+ Object.assign(viewProps, transformer.transform(viewProps, props));
+ }
+ let ViewComponent = viewSpec.component;
+ return (preact.createElement(ViewComponent, Object.assign({}, viewProps)));
+ }
+}
+function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) {
+ // don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
+ let todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason
+ let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false);
+ let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false);
+ return {
+ title,
+ activeButton: viewSpec.type,
+ navUnit: viewSpec.singleUnit,
+ isTodayEnabled: todayInfo.isValid && !internalCommon.rangeContainsMarker(dateProfile.currentRange, now),
+ isPrevEnabled: prevInfo.isValid,
+ isNextEnabled: nextInfo.isValid,
+ };
+}
+// Plugin
+// -----------------------------------------------------------------------------------------------------------------
+function buildViewPropTransformers(theClasses) {
+ return theClasses.map((TheClass) => new TheClass());
+}
+
+class Calendar extends internalCommon.CalendarImpl {
+ constructor(el, optionOverrides = {}) {
+ super();
+ this.isRendering = false;
+ this.isRendered = false;
+ this.currentClassNames = [];
+ this.customContentRenderId = 0;
+ this.handleAction = (action) => {
+ // actions we know we want to render immediately
+ switch (action.type) {
+ case 'SET_EVENT_DRAG':
+ case 'SET_EVENT_RESIZE':
+ this.renderRunner.tryDrain();
+ }
+ };
+ this.handleData = (data) => {
+ this.currentData = data;
+ this.renderRunner.request(data.calendarOptions.rerenderDelay);
+ };
+ this.handleRenderRequest = () => {
+ if (this.isRendering) {
+ this.isRendered = true;
+ let { currentData } = this;
+ internalCommon.flushSync(() => {
+ preact.render(preact.createElement(internalCommon.CalendarRoot, { options: currentData.calendarOptions, theme: currentData.theme, emitter: currentData.emitter }, (classNames, height, isHeightAuto, forPrint) => {
+ this.setClassNames(classNames);
+ this.setHeight(height);
+ return (preact.createElement(internalCommon.RenderId.Provider, { value: this.customContentRenderId },
+ preact.createElement(CalendarContent, Object.assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData))));
+ }), this.el);
+ });
+ }
+ else if (this.isRendered) {
+ this.isRendered = false;
+ preact.render(null, this.el);
+ this.setClassNames([]);
+ this.setHeight('');
+ }
+ };
+ internalCommon.ensureElHasStyles(el);
+ this.el = el;
+ this.renderRunner = new internalCommon.DelayedRunner(this.handleRenderRequest);
+ new CalendarDataManager({
+ optionOverrides,
+ calendarApi: this,
+ onAction: this.handleAction,
+ onData: this.handleData,
+ });
+ }
+ render() {
+ let wasRendering = this.isRendering;
+ if (!wasRendering) {
+ this.isRendering = true;
+ }
+ else {
+ this.customContentRenderId += 1;
+ }
+ this.renderRunner.request();
+ if (wasRendering) {
+ this.updateSize();
+ }
+ }
+ destroy() {
+ if (this.isRendering) {
+ this.isRendering = false;
+ this.renderRunner.request();
+ }
+ }
+ updateSize() {
+ internalCommon.flushSync(() => {
+ super.updateSize();
+ });
+ }
+ batchRendering(func) {
+ this.renderRunner.pause('batchRendering');
+ func();
+ this.renderRunner.resume('batchRendering');
+ }
+ pauseRendering() {
+ this.renderRunner.pause('pauseRendering');
+ }
+ resumeRendering() {
+ this.renderRunner.resume('pauseRendering', true);
+ }
+ resetOptions(optionOverrides, changedOptionNames) {
+ this.currentDataManager.resetOptions(optionOverrides, changedOptionNames);
+ }
+ setClassNames(classNames) {
+ if (!internalCommon.isArraysEqual(classNames, this.currentClassNames)) {
+ let { classList } = this.el;
+ for (let className of this.currentClassNames) {
+ classList.remove(className);
+ }
+ for (let className of classNames) {
+ classList.add(className);
+ }
+ this.currentClassNames = classNames;
+ }
+ }
+ setHeight(height) {
+ internalCommon.applyStyleProp(this.el, 'height', height);
+ }
+}
+
+function formatDate(dateInput, options = {}) {
+ let dateEnv = buildDateEnv(options);
+ let formatter = internalCommon.createFormatter(options);
+ let dateMeta = dateEnv.createMarkerMeta(dateInput);
+ if (!dateMeta) { // TODO: warning?
+ return '';
+ }
+ return dateEnv.format(dateMeta.marker, formatter, {
+ forcedTzo: dateMeta.forcedTzo,
+ });
+}
+function formatRange(startInput, endInput, options) {
+ let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object
+ let formatter = internalCommon.createFormatter(options);
+ let startMeta = dateEnv.createMarkerMeta(startInput);
+ let endMeta = dateEnv.createMarkerMeta(endInput);
+ if (!startMeta || !endMeta) { // TODO: warning?
+ return '';
+ }
+ return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
+ forcedStartTzo: startMeta.forcedTzo,
+ forcedEndTzo: endMeta.forcedTzo,
+ isEndExclusive: options.isEndExclusive,
+ defaultSeparator: internalCommon.BASE_OPTION_DEFAULTS.defaultRangeSeparator,
+ });
+}
+// TODO: more DRY and optimized
+function buildDateEnv(settings) {
+ let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
+ return new internalCommon.DateEnv(Object.assign(Object.assign({ timeZone: internalCommon.BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale }));
+}
+
+// HELPERS
+/*
+if nextDayThreshold is specified, slicing is done in an all-day fashion.
+you can get nextDayThreshold from context.nextDayThreshold
+*/
+function sliceEvents(props, allDay) {
+ return internalCommon.sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg;
+}
+
+const version = '6.1.15';
+
+exports.JsonRequestError = internalCommon.JsonRequestError;
+exports.Calendar = Calendar;
+exports.createPlugin = createPlugin;
+exports.formatDate = formatDate;
+exports.formatRange = formatRange;
+exports.globalLocales = globalLocales;
+exports.globalPlugins = globalPlugins;
+exports.sliceEvents = sliceEvents;
+exports.version = version;
+
+},{"./internal-common.cjs":2,"preact":11,"preact/compat":10}],2:[function(require,module,exports){
+'use strict';
+
+var preact = require('preact');
+var compat = require('preact/compat');
+
+function _interopNamespace(e) {
+ if (e && e.__esModule) return e;
+ var n = Object.create(null);
+ if (e) {
+ Object.keys(e).forEach(function (k) {
+ if (k !== 'default') {
+ var d = Object.getOwnPropertyDescriptor(e, k);
+ Object.defineProperty(n, k, d.get ? d : {
+ enumerable: true,
+ get: function () { return e[k]; }
+ });
+ }
+ });
+ }
+ n["default"] = e;
+ return Object.freeze(n);
+}
+
+var preact__namespace = /*#__PURE__*/_interopNamespace(preact);
+
+const styleTexts = [];
+const styleEls = new Map();
+function injectStyles(styleText) {
+ styleTexts.push(styleText);
+ styleEls.forEach((styleEl) => {
+ appendStylesTo(styleEl, styleText);
+ });
+}
+function ensureElHasStyles(el) {
+ if (el.isConnected && // sometimes true if SSR system simulates DOM
+ el.getRootNode // sometimes undefined if SSR system simulates DOM
+ ) {
+ registerStylesRoot(el.getRootNode());
+ }
+}
+function registerStylesRoot(rootNode) {
+ let styleEl = styleEls.get(rootNode);
+ if (!styleEl || !styleEl.isConnected) {
+ styleEl = rootNode.querySelector('style[data-fullcalendar]');
+ if (!styleEl) {
+ styleEl = document.createElement('style');
+ styleEl.setAttribute('data-fullcalendar', '');
+ const nonce = getNonceValue();
+ if (nonce) {
+ styleEl.nonce = nonce;
+ }
+ const parentEl = rootNode === document ? document.head : rootNode;
+ const insertBefore = rootNode === document
+ ? parentEl.querySelector('script,link[rel=stylesheet],link[as=style],style')
+ : parentEl.firstChild;
+ parentEl.insertBefore(styleEl, insertBefore);
+ }
+ styleEls.set(rootNode, styleEl);
+ hydrateStylesRoot(styleEl);
+ }
+}
+function hydrateStylesRoot(styleEl) {
+ for (const styleText of styleTexts) {
+ appendStylesTo(styleEl, styleText);
+ }
+}
+function appendStylesTo(styleEl, styleText) {
+ const { sheet } = styleEl;
+ const ruleCnt = sheet.cssRules.length;
+ styleText.split('}').forEach((styleStr, i) => {
+ styleStr = styleStr.trim();
+ if (styleStr) {
+ sheet.insertRule(styleStr + '}', ruleCnt + i);
+ }
+ });
+}
+// nonce
+// -------------------------------------------------------------------------------------------------
+let queriedNonceValue;
+function getNonceValue() {
+ if (queriedNonceValue === undefined) {
+ queriedNonceValue = queryNonceValue();
+ }
+ return queriedNonceValue;
+}
+/*
+TODO: discourage meta tag and instead put nonce attribute on placeholder