diff --git a/README.md b/README.md index d21092aadf77f367ee7a1fdbd586917017ced5e6..42224117efbc1c24e592495540922a16309c6f9d 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,13 @@ This module provides datetime field formatters to display the field as a timer or countdown. Module provides 3 field formatters: simple text and -2 formatters based on jQuery plugins: County and jQuery Countdown. - -## Requirements - -- [County](https://github.com/brilsergei/county) -- [jQuery Countdown](http://keith-wood.name/countdown.html) +2 formatters based on jQuery plugins: [County](https://github.com/brilsergei/county) +and [jQuery Countdown](http://keith-wood.name/countdown.html). ## Installation -1. Download the module. -2. Download required JS libraries to drupal library directory and rename -library directories to libraries/county and libraries/jquery.countdown -respectively. This module supports jQuery Countdown 2.1.0. -4. Enable the module using admin module page or drush. +1. Install module using composer or download the module archive. +2. Enable the module using admin module page or drush. ## Translation @@ -41,6 +34,13 @@ translate to and change in the copied file option 'regional' for your field. Formatter settings cannot be translated via interface yet. See https://www.drupal.org/project/drupal/issues/2546212 +## Assets +Required assets of the used JS libs are loaded from [jsDelivr](https://www.jsdelivr.com/). +It is also possible to load the assets from site local storage. In order to use locally stored library assets +download required JS libraries to drupal library directory and rename +library directories to libraries/county and libraries/jquery.countdown +respectively. This module supports jQuery Countdown 2.1.0. + ## Issues, Bugs and Feature Requests Issues, Bugs and Feature Requests should be made on the page at diff --git a/config/install/field_timer.config.yml b/config/install/field_timer.config.yml new file mode 100644 index 0000000000000000000000000000000000000000..32559a73afdd03e227224ba97410f451b7dc4e85 --- /dev/null +++ b/config/install/field_timer.config.yml @@ -0,0 +1 @@ +asset_source: 'js-delivr' diff --git a/config/schema/field_timer.schema.yml b/config/schema/field_timer.schema.yml index 19e30461f7a36e64bb3bbe869ca2effb5a805551..50ecdf174e345230b0f9ae236cff2cbac6a38005 100644 --- a/config/schema/field_timer.schema.yml +++ b/config/schema/field_timer.schema.yml @@ -1,3 +1,10 @@ +field_timer.config: + type: config_object + mapping: + asset_source: + type: string + label: 'Asset source' + field.formatter.settings.field_timer_simple_text: type: field.formatter.settings.datetime_time_ago label: 'Field Timer Text timer or countdown formatter display settings' diff --git a/css/field_timer-js-delivr.css b/css/field_timer-js-delivr.css new file mode 100644 index 0000000000000000000000000000000000000000..df2111682232c57b3170ce4f923ad189fb47b550 --- /dev/null +++ b/css/field_timer-js-delivr.css @@ -0,0 +1,6 @@ +.field-timer-jquery-countdown-led.green span { + background: url('https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/img/countdownLED.png') no-repeat 0px 0px; +} +.field-timer-jquery-countdown-led.blue span { + background: url('https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/img/countdownGlowing.gif') no-repeat 0 0; +} diff --git a/css/field_timer-local.css b/css/field_timer-local.css new file mode 100644 index 0000000000000000000000000000000000000000..45a521dda7e4d716e9c25150b3a5c40a76292594 --- /dev/null +++ b/css/field_timer-local.css @@ -0,0 +1,6 @@ +.field-timer-jquery-countdown-led.green span { + background: url('/libraries/jquery.countdown/img/countdownLED.png') no-repeat 0 0; +} +.field-timer-jquery-countdown-led.blue span { + background: url('/libraries/jquery.countdown/img/countdownGlowing.gif') no-repeat 0 0; +} diff --git a/css/field_timer.css b/css/field_timer.css index 5f1df8b32c1aa150a9d0cd0ca19459e67857708d..9ecc9b211e56435d89ead0c8b39254f453cfd177 100644 --- a/css/field_timer.css +++ b/css/field_timer.css @@ -39,14 +39,11 @@ .field-timer-jquery-countdown-led span.green.imageDay {background-position: -100px 0;} .field-timer-jquery-countdown-led span.green.imageSep {background-position: -110px 0;} .field-timer-jquery-countdown-led span.green.imageSpace {background-position: -120px 0;} -.field-timer-jquery-countdown-led.green span { - background: url('/libraries/jquery.countdown/img/countdownLED.png') no-repeat 0px 0px; -} .field-timer-jquery-countdown-led span.blue { display: block; float: left; width: 34px; - height: 50px; + height: 50px; } .field-timer-jquery-countdown-led span.blue.image0 {background-position: 0 0;} .field-timer-jquery-countdown-led span.blue.image1 {background-position: -34px 0;} @@ -62,6 +59,3 @@ .field-timer-jquery-countdown-led span.blue.imageSep {background-position: -374px 0;} .field-timer-jquery-countdown-led span.blue.imageSpace {background-position: -408px 0;} .field-timer-jquery-countdown-led.is-countdown {border: none;} -.field-timer-jquery-countdown-led.blue span { - background: url('/libraries/jquery.countdown/img/countdownGlowing.gif') no-repeat 0 0; -} diff --git a/field_timer.install b/field_timer.install new file mode 100644 index 0000000000000000000000000000000000000000..217a79a18f59d1d69655abd7720fe990083bb52a --- /dev/null +++ b/field_timer.install @@ -0,0 +1,13 @@ +<?php + +/** + * Create field_timer config. + */ +function field_timer_update_10201() { + $configFactory = \Drupal::configFactory(); + $config = $configFactory->getEditable('field_timer.config'); + // Set local as asset source on existing sites because site owners already + // has them downloaded and rely on local assets. + $config->set('asset_source', 'local'); + $config->save(TRUE); +} diff --git a/field_timer.libraries.yml b/field_timer.libraries.yml index 97b74380b6d755927720def31fff970a5df7f8a3..f8b22572a6f00fda55b5a0c4ad485fc4c1aba947 100644 --- a/field_timer.libraries.yml +++ b/field_timer.libraries.yml @@ -10,6 +10,7 @@ init: css: component: css/field_timer.css: {} + css/field_timer-local.css: {} county: remote: http://www.egrappler.com/free-jquery-count-down-plugin-county/ diff --git a/field_timer.links.menu.yml b/field_timer.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..11b38d35588427aac668a2795031243935c469dd --- /dev/null +++ b/field_timer.links.menu.yml @@ -0,0 +1,5 @@ +field_timer.config: + title: Field timer configuration + route_name: field_timer.config + description: Configure field timer asset source. + parent: system.admin_config_development diff --git a/field_timer.module b/field_timer.module new file mode 100644 index 0000000000000000000000000000000000000000..60706cccfb9599e04d8350e7a119e25454290c51 --- /dev/null +++ b/field_timer.module @@ -0,0 +1,18 @@ +<?php + +/** + * Implements hook_library_info_alter(). + */ +function field_timer_library_info_alter(&$libraries, $extension) { + if ($extension !== 'field_timer') { + return; + } + + $assetSource = \Drupal::config('field_timer.config')->get('asset_source'); + if (!$assetSource || $assetSource === 'local') { + return; + } + + $libraries = \Drupal::service('field_timer.library_asset_links') + ->replaceLocalWithJsDelivr($libraries); +} diff --git a/field_timer.routing.yml b/field_timer.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..8ef38da887ebf0c370f6994da65dee87ccd017e5 --- /dev/null +++ b/field_timer.routing.yml @@ -0,0 +1,7 @@ +field_timer.config: + path: /admin/config/development/field-timer + defaults: + _form: Drupal\field_timer\Form\ConfigForm + _title: 'Field timer configuration' + requirements: + _permission: 'administer site configuration' diff --git a/field_timer.services.yml b/field_timer.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..447ecef495762d713e5a6413fd1afd4e9111698b --- /dev/null +++ b/field_timer.services.yml @@ -0,0 +1,3 @@ +services: + field_timer.library_asset_links: + class: Drupal\field_timer\LibraryAssetLinks diff --git a/src/Form/ConfigForm.php b/src/Form/ConfigForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b3d554aed8e83db7303508efb53711f24af982a3 --- /dev/null +++ b/src/Form/ConfigForm.php @@ -0,0 +1,84 @@ +<?php + +namespace Drupal\field_timer\Form; + +use Drupal\Core\Asset\LibraryDiscoveryInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class ConfigForm extends ConfigFormBase { + + /** + * Library discovery service + * + * @var \Drupal\Core\Asset\LibraryDiscoveryInterface + */ + private $libraryDiscovery; + + /** + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $libraryDiscovery + */ + public function __construct(ConfigFactoryInterface $config_factory, LibraryDiscoveryInterface $libraryDiscovery) { + parent::__construct($config_factory); + + $this->libraryDiscovery = $libraryDiscovery; + } + + /** + * @inheritDoc + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('library.discovery') + ); + } + + /** + * @inheritDoc + */ + protected function getEditableConfigNames() { + return ['field_timer.config']; + } + + /** + * @inheritDoc + */ + public function getFormId() { + return 'field_timer_config_form'; + } + + /** + * @inheritDoc + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['asset_source'] = [ + '#type' => 'radios', + '#title' => $this->t('Asset source'), + '#options' => [ + 'local' => $this->t('Local directory (web/libraries)'), + 'js-delivr' => $this->t('CDN (jsDelivr)') + ], + '#default_value' => $this->config('field_timer.config') + ->get('asset_source'), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * @inheritDoc + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('field_timer.config') + ->set('asset_source', $form_state->getValue('asset_source')) + ->save(); + $this->libraryDiscovery->clearCachedDefinitions(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/src/LibraryAssetLinks.php b/src/LibraryAssetLinks.php new file mode 100644 index 0000000000000000000000000000000000000000..d6407d97ffb24c145cc18b9d38b278bc88010299 --- /dev/null +++ b/src/LibraryAssetLinks.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\field_timer; + +/** + * Helper which replaces asset links in library definitions. + * + * Asset links are replaced from local paths to remote paths. + */ +class LibraryAssetLinks { + + /** + * Replaces local asset links with links to jsDelivr. + * + * @param array $libraries + * + * @return array + */ + public function replaceLocalWithJsDelivr(array $libraries) { + // Use CSS which contains link to images in CDN. + unset($libraries['init']['css']['component']['css/field_timer-local.css']); + $libraries['init']['css']['component']['css/field_timer-js-delivr.css'] = []; + + // Replace links to local assets with links to assets in CDN. + foreach ($libraries['county']['js'] as $js => $data) { + $this->replaceLocalCountyWithJsDelivr($libraries['county']['js'], $js); + } + foreach ($libraries['county']['css']['component'] as $css => $data) { + $this->replaceLocalCountyWithJsDelivr($libraries['county']['css']['component'], $css); + } + foreach ($libraries['jquery.countdown']['js'] as $js => $data) { + $this->replaceLocalJqueryCountdownWithJsDelivr($libraries['jquery.countdown']['js'], $js); + } + foreach ($libraries['jquery.countdown']['css']['component'] as $css => $data) { + $this->replaceLocalJqueryCountdownWithJsDelivr($libraries['jquery.countdown']['css']['component'], $css); + } + foreach ($libraries as $name => $library) { + if (str_contains($name, 'jquery.countdown.')) { + foreach ($libraries[$name]['js'] as $js => $data) { + $this->replaceLocalJqueryCountdownWithJsDelivr($libraries[$name]['js'], $js); + } + } + } + + return $libraries; + } + + /** + * Replaces local asset links of County with links to jsDelivr. + * + * @param array $assetArray + * @param string $oldLink + * + * @return void + */ + protected function replaceLocalCountyWithJsDelivr(array &$assetArray, string $oldLink) { + $this->replaceLocalLinkWithJsDelivr($assetArray, $oldLink, '/libraries/county', 'https://cdn.jsdelivr.net/gh/brilsergei/county@0.0.1'); + } + + /** + * Replaces local asset links of Countdown with links to jsDelivr. + * + * @param array $assetArray + * @param string $oldLink + * + * @return void + */ + protected function replaceLocalJqueryCountdownWithJsDelivr(array &$assetArray, string $oldLink) { + $this->replaceLocalLinkWithJsDelivr($assetArray, $oldLink, '/libraries/jquery.countdown', 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist'); + } + + /** + * Replaces a local assets link with corresponding link to jsDelivr. + * + * @param array $assetArray + * @param string $oldLink + * @param string $search + * @param string $replace + * + * @return void + */ + protected function replaceLocalLinkWithJsDelivr(array &$assetArray, string $oldLink, string $search, string $replace) { + $data = $assetArray[$oldLink]; + unset($assetArray[$oldLink]); + $newLink = str_replace($search, $replace, $oldLink); + $data['type'] = 'external'; + $assetArray[$newLink] = $data; + } + +} diff --git a/tests/fixtures/update/drupal-10.0.2.bare.standard.field_timer-2.0.1.php.gz b/tests/fixtures/update/drupal-10.0.2.bare.standard.field_timer-2.0.1.php.gz new file mode 100644 index 0000000000000000000000000000000000000000..8db233257eb491a35781c99d2fee1217dd570b2f Binary files /dev/null and b/tests/fixtures/update/drupal-10.0.2.bare.standard.field_timer-2.0.1.php.gz differ diff --git a/tests/src/Functional/AssetSourceTest.php b/tests/src/Functional/AssetSourceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..764c1afbd231e7b9ccaf70c246d1c89ad83941cc --- /dev/null +++ b/tests/src/Functional/AssetSourceTest.php @@ -0,0 +1,179 @@ +<?php + +namespace Drupal\Tests\field_timer\Functional; + +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\Core\Datetime\Entity\DateFormat; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\BrowserTestBase; + +class AssetSourceTest extends BrowserTestBase { + + /** + * A field storage to use in this test class. + * + * @var \Drupal\field\Entity\FieldStorageConfig + */ + protected $fieldStorage; + + /** + * The field used in this test class. + * + * @var \Drupal\field\Entity\FieldConfig + */ + protected $field; + + /** + * @var string + */ + protected $fieldName = 'field_timer'; + + /** + * {@inheritdoc} + */ + protected static $modules = ['field_timer', 'entity_test', 'field_ui']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $web_user = $this->drupalCreateUser([ + 'access administration pages', + 'view test entity', + 'administer entity_test content', + 'administer entity_test fields', + 'administer entity_test display', + 'administer entity_test form display', + 'view the administration theme', + 'administer site configuration', + ]); + $this->drupalLogin($web_user); + + $type = 'datetime'; + $widgetType = 'datetime_default'; + + $this->fieldStorage = FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'entity_type' => 'entity_test', + 'type' => $type, + ]); + $this->fieldStorage->save(); + $this->field = FieldConfig::create([ + 'field_storage' => $this->fieldStorage, + 'bundle' => 'entity_test', + 'required' => TRUE, + ]); + $this->field->save(); + + EntityFormDisplay::load('entity_test.entity_test.default') + ->setComponent($this->fieldName, ['type' => $widgetType]) + ->save(); + + EntityViewDisplay::create([ + 'targetEntityType' => $this->field->getTargetEntityTypeId(), + 'bundle' => $this->field->getTargetBundle(), + 'mode' => 'full', + 'status' => TRUE, + ])->save(); + } + + /** + * @dataProvider assetSourcesDataProvider + */ + public function testAssetSources(string $formatter, array $configs, array $assets) { + // Configure display. + $this->drupalGet('entity_test/structure/entity_test/display/full'); + $fieldFormatterEdit = [ + 'fields[' . $this->fieldName . '][region]' => 'content', + 'fields[' . $this->fieldName . '][type]' => $formatter, + ]; + $this->submitForm($fieldFormatterEdit, 'Save'); + + // Configure formatter if required. + if (count($configs) > 0) { + $this->submitForm([], $this->fieldName . '_settings_edit'); + $edit = []; + $keyPrefix = 'fields[' . $this->fieldName + . '][settings_edit_form][settings]['; + foreach ($configs as $config => $value) { + $edit[$keyPrefix . $config . ']'] = $value; + } + $this->submitForm($edit, 'Update'); + $this->submitForm([], 'Save'); + } + + // Create an entity with datetime field. + $this->drupalGet('entity_test/add'); + $value = time() + 24 * 60 * 60; + $date = DrupalDateTime::createFromTimestamp($value); + $date_format = DateFormat::load('html_date')->getPattern(); + $time_format = DateFormat::load('html_time')->getPattern(); + $entityEdit = [ + $this->fieldName . '[0][value][date]' => $date->format($date_format), + $this->fieldName . '[0][value][time]' => $date->format($time_format), + ]; + $this->submitForm($entityEdit, 'Save'); + + // Make sure entity was created. + preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match); + $id = $match[1]; + $this->assertSession()->pageTextContains('entity_test ' . $id . ' has been created.'); + + // Make sure external assets for formatter are loaded to the page. + $this->drupalGet('entity_test/' . $id); + foreach ($assets as $asset) { + $this->assertSession() + ->responseContains($asset); + } + + // Enable usage of local assets. + $this->drupalGet('admin/config/development/field-timer'); + $this->submitForm(['asset_source' => 'local'], 'Save configuration'); + + // Make sure external assets for formatter are not loaded to the page. + $this->drupalGet('entity_test/' . $id); + foreach ($assets as $asset) { + $this->assertSession() + ->responseNotContains($asset); + } + } + + public function assetSourcesDataProvider() { + yield [ + 'field_timer_county', + [], + [ + 'https://cdn.jsdelivr.net/gh/brilsergei/county@0.0.1/js/county.js', + 'https://cdn.jsdelivr.net/gh/brilsergei/county@0.0.1/css/county.css', + ], + ]; + + yield [ + 'field_timer_countdown', + ['regional' => 'sr'], + [ + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.plugin.min.js', + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.countdown.min.js', + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/css/jquery.countdown.css', + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.countdown-sr.js', + ], + ]; + + yield [ + 'field_timer_countdown_led', + [], + ['css/field_timer-js-delivr.css'], + ]; + } + +} diff --git a/tests/src/Functional/Update/InitialConfigUpdate.php b/tests/src/Functional/Update/InitialConfigUpdate.php new file mode 100644 index 0000000000000000000000000000000000000000..81b3e4d7c9d08b8faebcdae34f107898d2f5cfeb --- /dev/null +++ b/tests/src/Functional/Update/InitialConfigUpdate.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\Tests\field_timer\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +class InitialConfigUpdate extends UpdatePathTestBase { + + /** + * @inheritDoc + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../fixtures/update/drupal-10.0.2.bare.standard.field_timer-2.0.1.php.gz', + ]; + } + + public function testConfigCreation() { + $config = $this->config('field_timer.config'); + $this->assertEmpty($config->get('asset_source')); + + // Run updates. + $this->runUpdates(); + + $config = $this->config('field_timer.config'); + $this->assertSame('local', $config->get('asset_source')); + } + +} diff --git a/tests/src/Unit/LibraryAssetLinksTest.php b/tests/src/Unit/LibraryAssetLinksTest.php new file mode 100644 index 0000000000000000000000000000000000000000..db364b7c41f97847f69b90fa3ebef850b19e699e --- /dev/null +++ b/tests/src/Unit/LibraryAssetLinksTest.php @@ -0,0 +1,91 @@ +<?php + +namespace Drupal\Tests\field_timer\Unit; + +use Drupal\field_timer\LibraryAssetLinks; +use Drupal\Tests\UnitTestCase; + +class LibraryAssetLinksTest extends UnitTestCase { + + /** + * @dataProvider replaceLocalWithJsDelivrDataProvider + */ + public function testReplaceLocalWithJsDelivr(array $libraries, array $expected) { + $libraryAssetLinks = new LibraryAssetLinks(); + $result = $libraryAssetLinks->replaceLocalWithJsDelivr($libraries); + $this->assertEquals($expected, $result); + } + + public function replaceLocalWithJsDelivrDataProvider() { + $libraries = [ + 'init' => [ + 'css' => ['component' => ['css/field_timer-local.css' => []]], + ], + 'county' => [ + 'js' => ['/libraries/county/js/county.js' => []], + 'css' => ['component' => ['/libraries/county/css/county.css' => []]], + ], + 'jquery.countdown' => [ + 'js' => [ + '/libraries/jquery.countdown/js/jquery.plugin.min.js' => [], + '/libraries/jquery.countdown/js/jquery.countdown.min.js' => [], + ], + 'css' => [ + 'component' => [ + '/libraries/jquery.countdown/css/jquery.countdown.css' => [], + ], + ], + ], + 'jquery.countdown.hy' => [ + 'js' => ['/libraries/jquery.countdown/js/jquery.countdown-hy.js' => []], + ], + ]; + + $expected = [ + 'init' => [ + 'css' => ['component' => ['css/field_timer-js-delivr.css' => []]], + ], + 'county' => [ + 'js' => [ + 'https://cdn.jsdelivr.net/gh/brilsergei/county@0.0.1/js/county.js' => [ + 'type' => 'external', + ], + ], + 'css' => [ + 'component' => [ + 'https://cdn.jsdelivr.net/gh/brilsergei/county@0.0.1/css/county.css' => [ + 'type' => 'external', + ], + ], + ], + ], + 'jquery.countdown' => [ + 'js' => [ + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.plugin.min.js' => [ + 'type' => 'external', + ], + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.countdown.min.js' => [ + 'type' => 'external', + ], + ], + 'css' => [ + 'component' => [ + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/css/jquery.countdown.css' => [ + 'type' => 'external', + ], + ], + ], + ], + 'jquery.countdown.hy' => [ + 'js' => [ + 'https://cdn.jsdelivr.net/gh/kbwood/countdown@2.1.0/dist/js/jquery.countdown-hy.js' => [ + 'type' => 'external', + ], + ], + ], + ]; + + yield [$libraries, $expected]; + } + +}