Browse Source

Add Datalist widget.

mscherer 7 years ago
parent
commit
401430cd5f

+ 72 - 0
docs/Component/Common.md

@@ -0,0 +1,72 @@
+# Common component
+
+## Trimming payload data
+By default, adding the Common component to your AppController will make sure your POST and query params are trimmed.
+This is needed to make - not only notEmpty - validation working properly.
+
+You can skip for certain actions using `'DataPreparation.notrim'` config key per use case.
+
+## Is Post Check
+A convenience method can quickly check on a form being posted:
+```php
+if ($this->Common->isPosted()) {}
+```
+Saves you the trouble of checking for `post`, `patch`, `put` etc together, and in most cases this is not necessary. It is only important it wasn't a `get` request.
+
+## Secure redirects back
+Sometimes you want to post to an edit or delete form and make sure you get redirected back to the correct action including query strings (e.g. for filtering).
+Then you can pass `redirect` key as either as part of POST payload or as query string.
+
+```
+// In your action
+$redirectUrl = $this->Common->getSafeRedirectUrl(['action' => 'default']);
+return $this->redirect($redirectUrl);
+```
+
+It is important to not use the payload data without sanitation for security reasons (redirect forgery to external websites).
+
+## Default URL params
+
+`CommonComponent::defaultUrlParams()` will give you the default params you might want to combine with your URL 
+in order to always generate the right URLs even if inside plugins or prefixes.
+
+## Current URL
+
+`$this->Common->currentUrl()` returns current url (with all missing params automatically added).
+
+## autoRedirect()
+A shortcut convenience wrapper for referrer redirecting with fallback:
+```php
+return $this->Common->autoRedirect($defaultUrl);
+```
+Set the 2nd param to true to allow redirecting to itself (if that was the referer).
+
+## completeRedirect()
+Automatically also adds the query string into the redirect. Useful when you want to keep the filters and pass them around.
+```php
+return $this->Common->completeRedirect($redirectUrl);
+```
+
+ 
+## getPassedParam()
+Convenience method to get passed params:
+```php
+$param = $this->Common->getPassedParam('x', $default);
+```
+
+## isForeignReferer()
+Check if a certain referer is a non local one:
+```php
+// using env referer
+$result = $this->Common->isForeignReferer();
+// or explicit value
+$result = $this->Common->isForeignReferer($urlString);
+```
+
+ 
+ ## Listing actions
+ 
+ If you need all current (direct) actions of a controller, call
+ ```php
+ $this->Common->listActions()
+ ```

+ 18 - 0
docs/Component/Mobile.md

@@ -0,0 +1,18 @@
+# Mobile component
+
+The mobile component can hold the information of whether to serve a mobile layout based on session preference and browser headers.
+```php
+// In your controller (action)
+$isMobile = $this->Mobile->isMobile();
+```
+
+You can provide a form/button in the bottom of the layout that can switch a session variable to overwrite the browser detection:
+Just store the user's choice in the `'User.mobile'` session key.
+
+## Configuration
+
+	'on' => 'beforeFilter', // initialize (prior to controller's beforeRender) or startup
+	'engine' => null, // CakePHP internal if null
+	'themed' => false, // If false uses subfolders instead of themes: /View/.../mobile/
+	'auto' => false, // auto set mobile views
+	

+ 45 - 0
docs/Helper/Common.md

@@ -0,0 +1,45 @@
+# Common Helper
+
+A CakePHP helper to handle some common topics.
+
+### Setup
+Include helper in your AppView class as
+```php
+$this->addHelper('Tools.Common', [
+	...
+]);
+```
+
+### Singular vs Plural
+```php
+echo $this->Common->sp('Singular', 'Plural', $count, true);
+```
+If using explicit translations or if no I18n translation is necessary, you don't need the 4th argument:
+
+```php
+echo $this->Common->sp(__('Singular'), __('Plural'), $count);
+```
+
+### Meta tags
+
+Canonical URL:
+```php
+echo $this->Format->metaCanonical($url);
+```
+
+Alternate content URL:
+
+```php
+echo $this->Format->metaAlternate($url, $language);
+```
+
+RSS link:
+```php
+echo $this->Format->metaRss($url, $title);
+```
+
+Generic meta tags:
+
+```php
+echo $this->Format->metaEquiv($type, $value, $escape)
+```

+ 58 - 0
docs/Helper/Format.md

@@ -0,0 +1,58 @@
+# Format Helper
+
+A CakePHP helper to handle some common format topics.
+
+### Setup
+Include helper in your AppView class as
+```php
+$this->addHelper('Tools.Format', [
+	...
+]);
+```
+
+You can store default configs also in Configure key `'Format'`.
+
+
+### icon()
+Display font icons using the default namespace or an already prefixed one.
+```php
+echo $this->Html->link(
+	$this->Format->icon('view'), 
+	$url, 
+	$attributes
+);
+```
+
+You can alias them via Configure for more usability:
+```php
+// In app.php
+	'Format' => [
+		'fontIcons' => [
+			'login' => 'fa fa-sign-in',
+			'logout' => 'fa fa-sign-out',
+			'translate' => 'fa fa-language',
+		],
+	],
+	
+// in the template
+echo $this->Format->icon('translate', ['title' => 'Translate this']);
+```
+
+### yesNo()
+
+Displays yes/no symbol for e.g. boolean values as more user friendly representation.
+
+### ok()
+
+Display a colored result based on the 2nd argument being true or false.
+```php
+echo $this->Format->ok($text, $bool, $optionalAttributes);
+```
+
+### disabledLink()
+
+Display a disabled link with a default title.
+
+### array2table()
+
+Translate a result array into a HTML table.

docs/Network/Email.md → docs/Mailer/Email.md


+ 22 - 20
docs/README.md

@@ -1,7 +1,5 @@
 # CakePHP Tools Plugin Documentation
 
-## Version notice
-
 ## Installation
 * [Installation](Install.md)
 
@@ -11,23 +9,25 @@
 ## Detailed Documentation - Quicklinks
 
 Routing:
-* [Url](Url/Url.md)
+* [Url](Url/Url.md) for useful tooling around URL generation.
 
 I18n:
 * [I18n](I18n/I18n.md) for language detection and switching
 
 ErrorHandler
-* [ErrorHandler](Error/ErrorHandler.md)
+* [ErrorHandler](Error/ErrorHandler.md) for improved error handling.
 
 Auth
 * [MultiColumnAuthenticate](Auth/MultiColumn.md) for log-in with e.g. "email or username"
 
+Email
+* [Email](Mailer/Email.md) for sending Emails
+
 Testing
-* [Testing](TestSuite/Testing.md)
+* [Testing](TestSuite/Testing.md) for testing tooling.
 
-Helpers:
-* [Html](Helper/Html.md)
-* [Form](Helper/Form.md)
+Controller:
+* [Controller](Controller/Controller.md)
 
 Behaviors:
 * [Jsonable](Behavior/Jsonable.md)
@@ -38,6 +38,20 @@ Behaviors:
 * [String](Behavior/String.md)
 * [Toggle](Behavior/Toggle.md)
 
+Components:
+* [Common](Component/Common.md)
+* [Mobile](Component/Mobile.md)
+
+Helpers:
+* [Html](Helper/Html.md)
+* [Form](Helper/Form.md)
+* [Common](Helper/Common.md)
+* [Format](Helper/Format.md)
+* [Typography](Helper/Typography.md)
+
+Widgets:
+* [Datalist](Widget/Datalist.md)
+
 ## Basic enhancements of the core
 
 ### Model
@@ -93,18 +107,6 @@ can be reused immediatelly without refactoring them right away.
 
 * See [Shims](Shims.md) for details.
 
-## Testing the Plugin
-You can test using a local installation of phpunit or the phar version of it:
-
-	cd plugins/Tools
-	composer update // or: php composer.phar update
-	phpunit // or: php phpunit.phar
-
-To test a specific file:
-
-	phpunit /path/to/class.php
-
-
 ## Contributing
 Your help is greatly appreciated.
 

+ 2 - 0
docs/Upgrade.md

@@ -32,3 +32,5 @@
 ### JsonableBehavior
 - No auto-detect anymore, fields need to be specified manually
 
+## Rss View
+- This has been moved to [cakephp-feed plugin](https://github.com/dereuromark/cakephp-feed).

+ 0 - 3
docs/View/Rss.md

@@ -1,3 +0,0 @@
-# Rss View
-
-This has been moved to [cakephp-feed plugin](https://github.com/dereuromark/cakephp-feed).

+ 45 - 0
docs/Widget/Datalist.md

@@ -0,0 +1,45 @@
+# Datalist Widget 
+
+Many of the HTML 5 new widgets are automatically supported by CakePHP.
+Unfortunatelly [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist) is not supported by default.
+
+This widget adds support for basic datalist support using the values or keys of the options array.
+If you need shimming for older browsers, add your JS snippet for this as polyfill yourself.
+
+### Setup
+
+Enable the following widget in your Form helper config:
+```php
+'datalist' => ['Tools\View\Widget\DatalistWidget'],
+```
+
+Add the following template to your Form helper templates:
+```php
+'datalist' => '<input type="text" list="datalist-{{id}}"{{inputAttrs}}><datalist id="datalist-{{id}}"{{datalistAttrs}}>{{content}}</datalist>',
+```
+
+Config:
+ - keys: Use as true to use the keys of the select options instead of the values.
+ - input: Attributes for input element
+
+### Usage
+
+```php
+echo $this->Form->control('search', ['type' => 'datalist', 'options' => $options]);
+```
+
+It will generate the above input with a datalist element.
+
+If you want to use the keys instead of the values:
+```php
+echo $this->Form->control('search', ['type' => 'datalist', 'options' => $options, 'keys' => true]);
+```
+
+You can pass input attributes using the `'input'` key, e.g. 
+`'input' => ['placeholder' => 'My placeholder']`.
+
+### Advanced usage
+You could also auto-create new entries right away.
+See [rrd108/cakephp-datalist](https://github.com/rrd108/cakephp-datalist) for this.
+
+Credits for this widget go to him for discovering the basics here.

+ 78 - 0
src/View/Widget/DatalistWidget.php

@@ -0,0 +1,78 @@
+<?php
+namespace Tools\View\Widget;
+
+use Cake\Utility\Text;
+use Cake\View\Form\ContextInterface;
+use Cake\View\Widget\SelectBoxWidget;
+
+/**
+ * Datalist widget
+ *
+ * See /docs for usage.
+ *
+ * Additional config:
+ * - keys: Use as true to use the keys of the select options instead of the values.
+ * - input: Attributes for input element
+ */
+class DatalistWidget extends SelectBoxWidget
+{
+    /**
+     * @param array $data
+     * @param \Cake\View\Form\ContextInterface $context
+     * @return string|null
+     */
+    public function render(array $data, ContextInterface $context)
+    {
+        $data += [
+            'id' => null,
+            'name' => '',
+            'empty' => false,
+            'escape' => true,
+            'options' => [],
+            'disabled' => null,
+            'val' => null,
+            'input' => [],
+            'keys' => false,
+            'templateVars' => [],
+        ];
+
+        $options = $this->_renderContent($data);
+        if (!$data['keys']) {
+            $options = str_replace(
+                'value',
+                'data-value',
+                $options
+            );
+        }
+
+        $name = $data['name'];
+        $id = $data['id'] ?: Text::slug($name);
+        $default = isset($data['val']) ? $data['val'] : null;
+
+        $inputData = $data['input'] + [
+            'id' => $id,
+            'name' => $name,
+            'autocomplete' => 'off',
+        ];
+
+        unset($data['name'], $data['options'], $data['empty'], $data['val'], $data['escape'], $data['keys'], $data['input'], $data['id']);
+        if (isset($data['disabled']) && is_array($data['disabled'])) {
+            unset($data['disabled']);
+        }
+
+        $inputData['value'] = $default;
+        $inputAttrs = $this->_templates->formatAttributes($inputData);
+
+        $datalistAttrs = $this->_templates->formatAttributes($data);
+        return $this->_templates->format(
+            'datalist',
+            [
+                'name' => $name,
+                'inputAttrs' => $inputAttrs,
+                'datalistAttrs' => $datalistAttrs,
+                'content' => implode('', $options),
+                'id' => $id,
+            ]
+        );
+    }
+}

+ 7 - 8
tests/TestCase/Controller/Component/CommonComponentTest.php

@@ -141,7 +141,7 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->postRedirect(['action' => 'foo']);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/foo', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**
@@ -151,7 +151,7 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->autoRedirect(['action' => 'foo']);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/foo', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**
@@ -163,7 +163,7 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->autoRedirect(['action' => 'foo'], true);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/my_controller/some-referer-action', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**
@@ -173,7 +173,7 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->autoPostRedirect(['action' => 'foo'], true);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/foo', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**
@@ -185,14 +185,13 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->autoPostRedirect(['controller' => 'MyController', 'action' => 'foo'], true);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/my_controller/allowed', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**
 	 * @return void
 	 */
-	public function testListActions()
-	{
+	public function testListActions() {
 		$actions = $this->Controller->Common->listActions();
 		$this->assertSame([], $actions);
 	}
@@ -206,7 +205,7 @@ class CommonComponentTest extends TestCase {
 		$is = $this->Controller->Common->autoPostRedirect(['controller' => 'MyController', 'action' => 'foo'], true);
 		$is = $this->Controller->response->header();
 		$this->assertSame('http://localhost/my_controller/foo', $is['Location']);
-		$this->assertSame(302, $this->Controller->response->statusCode());
+		$this->assertSame(302, $this->Controller->response->getStatusCode());
 	}
 
 	/**

+ 242 - 0
tests/TestCase/View/Widget/DatalistWidgetTest.php

@@ -0,0 +1,242 @@
+<?php
+namespace Tools\Test\TestCase\View\Widget;
+
+use Cake\TestSuite\TestCase;
+use Cake\View\StringTemplate;
+use Tools\View\Widget\DatalistWidget;
+
+class DatalistWidgetTest extends TestCase
+{
+	/**
+	 * @var \Cake\View\Form\ContextInterface
+	 */
+	protected $context;
+
+	/**
+	 * @var \Cake\View\StringTemplate
+	 */
+	protected $templates;
+
+    /**
+     * setup method.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $templates = [
+			'datalist' => '<input type="text" list="datalist-{{id}}"{{inputAttrs}}><datalist id="datalist-{{id}}"{{datalistAttrs}}>{{content}}</datalist>',
+            'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
+            'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
+            'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
+        ];
+        $this->context = $this->getMockBuilder('Cake\View\Form\ContextInterface')->getMock();
+        $this->templates = new StringTemplate($templates);
+    }
+
+    /**
+     * test render no options
+     *
+     * @return void
+     */
+    public function testRenderNoOptions()
+    {
+        $select = new DatalistWidget($this->templates);
+        $data = [
+            'id' => 'BirdName',
+            'name' => 'Birds[name]',
+            'options' => []
+        ];
+        $result = $select->render($data, $this->context);
+        $expected = [
+            'input' => ['type' => 'text', 'list' => 'datalist-BirdName', 'id' => 'BirdName', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-BirdName'],
+            '/datalist'
+        ];
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
+     * test simple rendering
+     *
+     * @return void
+     */
+    public function testRenderSimple()
+    {
+        $select = new DatalistWidget($this->templates);
+        $data = [
+            'id' => 'BirdName',
+            'name' => 'Birds[name]',
+            'options' => ['a' => 'Albatross', 'b' => 'Budgie']
+        ];
+        $result = $select->render($data, $this->context);
+		$expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-BirdName', 'id' => 'BirdName', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-BirdName'],
+			['option' => ['data-value' => 'a']], 'Albatross', '/option',
+			['option' => ['data-value' => 'b']], 'Budgie', '/option',
+			'/datalist'
+		];
+		$this->assertHtml($expected, $result);
+    }
+
+    /**
+     * test simple iterator rendering
+     *
+     * @return void
+     */
+    public function testRenderSimpleIterator()
+    {
+        $select = new DatalistWidget($this->templates);
+        $options = new \ArrayObject(['a' => 'Albatross', 'b' => 'Budgie']);
+        $data = [
+            'name' => 'Birds[name]',
+            'options' => $options,
+        ];
+        $result = $select->render($data, $this->context);
+		$expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+			['option' => ['data-value' => 'a']], 'Albatross', '/option',
+			['option' => ['data-value' => 'b']], 'Budgie', '/option',
+			'/datalist'
+		];
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
+     * test rendering with a selected value
+     *
+     * @return void
+     */
+    public function testRenderSelected()
+    {
+        $select = new DatalistWidget($this->templates);
+        $data = [
+            'name' => 'Birds[name]',
+            'val' => '1',
+            'options' => [
+                1 => 'one',
+                '1x' => 'one x',
+                '2' => 'two',
+                '2x' => 'two x',
+            ]
+        ];
+        $result = $select->render($data, $this->context);
+        $expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off', 'value' => '1'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+			['option' => ['data-value' => '1', 'selected' => 'selected']], 'one', '/option',
+            ['option' => ['data-value' => '1x']], 'one x', '/option',
+            ['option' => ['data-value' => '2']], 'two', '/option',
+            ['option' => ['data-value' => '2x']], 'two x', '/option',
+			'/datalist'
+        ];
+        $this->assertHtml($expected, $result);
+
+        $data['val'] = 2;
+        $result = $select->render($data, $this->context);
+		$expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off', 'value' => '2'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+			['option' => ['data-value' => '1']], 'one', '/option',
+			['option' => ['data-value' => '1x']], 'one x', '/option',
+			['option' => ['data-value' => '2', 'selected' => 'selected']], 'two', '/option',
+			['option' => ['data-value' => '2x']], 'two x', '/option',
+			'/datalist'
+		];
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
+     * test rendering with option groups
+     *
+     * @return void
+     */
+    public function testRenderOptionGroups()
+    {
+        $select = new DatalistWidget($this->templates);
+        $data = [
+            'name' => 'Birds[name]',
+            'options' => [
+                'Mammal' => [
+                    'beaver' => 'Beaver',
+                    'elk' => 'Elk',
+                ],
+                'Bird' => [
+                    'budgie' => 'Budgie',
+                    'eagle' => 'Eagle',
+                ]
+            ]
+        ];
+        $result = $select->render($data, $this->context);
+		$expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+			['optgroup' => ['label' => 'Mammal']],
+			['option' => ['data-value' => 'beaver']],
+			'Beaver',
+			'/option',
+			['option' => ['data-value' => 'elk']],
+			'Elk',
+			'/option',
+			'/optgroup',
+			['optgroup' => ['label' => 'Bird']],
+			['option' => ['data-value' => 'budgie']],
+			'Budgie',
+			'/option',
+			['option' => ['data-value' => 'eagle']],
+			'Eagle',
+			'/option',
+			'/optgroup',
+			'/datalist'
+		];
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
+     * test rendering with option groups and escaping
+     *
+     * @return void
+     */
+    public function testRenderOptionGroupsEscape()
+    {
+        $select = new DatalistWidget($this->templates);
+        $data = [
+            'name' => 'Birds[name]',
+            'options' => [
+                '>XSS<' => [
+                    '1' => 'One>',
+                ],
+            ]
+        ];
+        $result = $select->render($data, $this->context);
+        $expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+            ['optgroup' => ['label' => '&gt;XSS&lt;']],
+            ['option' => ['data-value' => '1']],
+            'One&gt;',
+            '/option',
+            '/optgroup',
+			'/datalist'
+        ];
+        $this->assertHtml($expected, $result);
+
+        $data['escape'] = false;
+        $result = $select->render($data, $this->context);
+        $expected = [
+			'input' => ['type' => 'text', 'list' => 'datalist-Birds-name', 'id' => 'Birds-name', 'name' => 'Birds[name]', 'autocomplete' => 'off'],
+			'datalist' => ['id' => 'datalist-Birds-name'],
+            ['optgroup' => ['label' => '>XSS<']],
+            ['option' => ['data-value' => '1']],
+            'One>',
+            '/option',
+            '/optgroup',
+			'/datalist'
+        ];
+        $this->assertHtml($expected, $result);
+    }
+
+}