Skip to content

Commit bdaaf97

Browse files
damienalexandreNyholm
authored andcommitted
Add Edit In Place functionnality (#23)
* Add Edit In Place functionnality * Move the login to a custom translator, allow {% trans %} to work This could not be done in a Twig Extension because TwigBundle have some hard coded FQN in the compiler. So now we have: - a Twig extension to mark filters as HTML safe - a Translator injected in the trans extension which output HTML * Implement a very basic "transChoice" call, without "selector" * Remove the hardcoded config name, make it configurable * Add an alert() message when something bad happend * Rewrite the Activator to simplify it, no more token, only session * Remove cache headers from the Response * Prevent editable label elements from losing focus * Add bacis validation and model * Add tests to verify that the activator works as expected * Make the Activator Service configurable * If the translation key finish by `.html`, use the HTML editor
1 parent 83a9fe0 commit bdaaf97

27 files changed

+1020
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <tobias.nyholm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Bundle\Controller;
13+
14+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Translation\Bundle\Exception\MessageValidationException;
18+
use Translation\Bundle\Model\EditInPlaceMessage;
19+
use Translation\Common\Model\Message;
20+
21+
/**
22+
* @author Damien Alexandre <dalexandre@jolicode.com>
23+
*/
24+
class EditInPlaceController extends Controller
25+
{
26+
/**
27+
* @param Request $request
28+
* @param string $configName
29+
* @param string $locale
30+
*
31+
* @return Response
32+
*/
33+
public function editAction(Request $request, $configName, $locale)
34+
{
35+
try {
36+
$messages = $this->getMessages($request, ['Edit']);
37+
} catch (MessageValidationException $e) {
38+
return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST);
39+
}
40+
41+
foreach ($messages as $message) {
42+
$this->get('php_translation.storage.'.$configName)->update(
43+
new Message($message->getKey(), $message->getDomain(), $locale, $message->getMessage())
44+
);
45+
}
46+
47+
return new Response();
48+
}
49+
50+
/**
51+
* Get and validate messages from the request.
52+
*
53+
* @param Request $request
54+
* @param array $validationGroups
55+
*
56+
* @return EditInPlaceMessage[]
57+
*
58+
* @throws MessageValidationException
59+
*/
60+
private function getMessages(Request $request, array $validationGroups = [])
61+
{
62+
$json = $request->getContent();
63+
$data = json_decode($json, true);
64+
$messages = [];
65+
$validator = $this->get('validator');
66+
67+
foreach ($data as $key => $value) {
68+
list($domain, $translationKey) = explode('|', $key);
69+
70+
$message = new EditInPlaceMessage();
71+
$message->setKey($translationKey);
72+
$message->setMessage($value);
73+
$message->setDomain($domain);
74+
75+
$errors = $validator->validate($message, null, $validationGroups);
76+
if (count($errors) > 0) {
77+
throw MessageValidationException::create();
78+
}
79+
80+
$messages[] = $message;
81+
}
82+
83+
return $messages;
84+
}
85+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <tobias.nyholm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Bundle\DependencyInjection\CompilerPass;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
/**
20+
* @author Damien Alexandre <dalexandre@jolicode.com>
21+
*/
22+
class EditInPlacePass implements CompilerPassInterface
23+
{
24+
public function process(ContainerBuilder $container)
25+
{
26+
/* @var Definition $def */
27+
if (!$container->hasDefinition('php_translator.edit_in_place.xtrans_html_translator')) {
28+
return;
29+
}
30+
31+
// Replace the Twig Translator by a custom HTML one
32+
$container->getDefinition('twig.extension.trans')->replaceArgument(
33+
0,
34+
new Reference('php_translator.edit_in_place.xtrans_html_translator')
35+
);
36+
}
37+
}

DependencyInjection/Configuration.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ public function getConfigTreeBuilder()
5858
->booleanNode('allow_add')->defaultTrue()->end()
5959
->end()
6060
->end()
61+
->arrayNode('edit_in_place')
62+
->canBeEnabled()
63+
->children()
64+
->scalarNode('config_name')->defaultValue('default')->end()
65+
->scalarNode('activator')->cannotBeEmpty()->defaultValue('php_translation.edit_in_place.activator')->end()
66+
->end()
67+
->end()
6168
->scalarNode('http_client')->cannotBeEmpty()->defaultValue('httplug.client')->end()
6269
->scalarNode('message_factory')->cannotBeEmpty()->defaultValue('httplug.message_factory')->end()
6370
->end();

DependencyInjection/TranslationExtension.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\ContainerBuilder;
1515
use Symfony\Component\Config\FileLocator;
1616
use Symfony\Component\DependencyInjection\DefinitionDecorator;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1718
use Symfony\Component\DependencyInjection\Reference;
1819
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
1920
use Symfony\Component\DependencyInjection\Loader;
@@ -55,6 +56,11 @@ public function load(array $configs, ContainerBuilder $container)
5556
$this->enableSymfonyProfiler($container, $config);
5657
}
5758

59+
if ($config['edit_in_place']['enabled']) {
60+
$loader->load('edit_in_place.yml');
61+
$this->enableEditInPlace($container, $config);
62+
}
63+
5864
if ($config['fallback_translation']['enabled']) {
5965
$loader->load('auto_translation.yml');
6066
$this->enableFallbackAutoTranslator($container, $config);
@@ -104,6 +110,24 @@ private function enableWebUi(ContainerBuilder $container, $config)
104110
{
105111
}
106112

113+
private function enableEditInPlace(ContainerBuilder $container, $config)
114+
{
115+
$name = $config['edit_in_place']['config_name'];
116+
117+
if ($name !== 'default' and !isset($config['configs'][$name])) {
118+
throw new InvalidArgumentException(sprintf('There is no config named "%s".', $name));
119+
}
120+
121+
$activatorRef = new Reference($config['edit_in_place']['activator']);
122+
123+
$def = $container->getDefinition('php_translation.edit_in_place.response_listener');
124+
$def->replaceArgument(0, $activatorRef);
125+
$def->replaceArgument(3, $name);
126+
127+
$def = $container->getDefinition('php_translator.edit_in_place.xtrans_html_translator');
128+
$def->replaceArgument(1, $activatorRef);
129+
}
130+
107131
private function enableSymfonyProfiler(ContainerBuilder $container, $config)
108132
{
109133
$container->setParameter('php_translation.toolbar.allow_edit', $config['symfony_profiler']['allow_edit']);

EditInPlace/Activator.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <tobias.nyholm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Bundle\EditInPlace;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Session\Session;
16+
17+
/**
18+
* Default Activator implementation.
19+
*
20+
* @author Damien Alexandre <dalexandre@jolicode.com>
21+
*/
22+
class Activator implements ActivatorInterface
23+
{
24+
const KEY = 'translation_bundle.edit_in_place.enabled';
25+
26+
/**
27+
* @var Session
28+
*/
29+
private $session;
30+
31+
public function __construct(Session $session)
32+
{
33+
$this->session = $session;
34+
}
35+
36+
/**
37+
* Enable the Edit In Place mode.
38+
*/
39+
public function activate()
40+
{
41+
$this->session->set(self::KEY, true);
42+
}
43+
44+
/**
45+
* Disable the Edit In Place mode.
46+
*/
47+
public function deactivate()
48+
{
49+
$this->session->remove(self::KEY);
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*
55+
* @todo Cache this call result for performance?
56+
*/
57+
public function checkRequest(Request $request = null)
58+
{
59+
if (!$this->session->has(self::KEY)) {
60+
return false;
61+
}
62+
63+
return $this->session->has(self::KEY);
64+
}
65+
}

EditInPlace/ActivatorInterface.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <tobias.nyholm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Bundle\EditInPlace;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
16+
/**
17+
* @author Damien Alexandre <dalexandre@jolicode.com>
18+
*/
19+
interface ActivatorInterface
20+
{
21+
/**
22+
* Tells if the Edit In Place mode is enabled for this request.
23+
*
24+
* @param Request|null $request
25+
*
26+
* @return bool
27+
*/
28+
public function checkRequest(Request $request = null);
29+
}

EditInPlace/ResponseListener.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <tobias.nyholm@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Translation\Bundle\EditInPlace;
13+
14+
use Symfony\Component\Asset\Packages;
15+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
16+
use Symfony\Component\Routing\Router;
17+
18+
/**
19+
* Adds Javascript/CSS files to the Response if the Activator returns true.
20+
*
21+
* @author Damien Alexandre <dalexandre@jolicode.com>
22+
*/
23+
class ResponseListener
24+
{
25+
const HTML = <<<'HTML'
26+
<!-- TranslationBundle -->
27+
<link rel="stylesheet" type="text/css" href="%s">
28+
29+
<script type="text/javascript" src="%s"></script>
30+
<script type="text/javascript" src="%s"></script>
31+
32+
<script type="text/javascript">
33+
window.onload = function() {
34+
TranslationBundleEditInPlace("%s");
35+
}
36+
</script>
37+
<!-- /TranslationBundle -->
38+
HTML;
39+
40+
/**
41+
* @var ActivatorInterface
42+
*/
43+
private $activator;
44+
45+
/**
46+
* @var Router
47+
*/
48+
private $router;
49+
50+
/**
51+
* @var Packages
52+
*/
53+
private $packages;
54+
55+
/**
56+
* @var string
57+
*/
58+
private $configName;
59+
60+
public function __construct(ActivatorInterface $activator, Router $router, Packages $packages, $configName = 'default')
61+
{
62+
$this->activator = $activator;
63+
$this->router = $router;
64+
$this->packages = $packages;
65+
$this->configName = $configName;
66+
}
67+
68+
public function onKernelResponse(FilterResponseEvent $event)
69+
{
70+
$request = $event->getRequest();
71+
72+
if ($this->activator->checkRequest($request)) {
73+
$content = $event->getResponse()->getContent();
74+
75+
// Clean the content for malformed tags in attributes or encoded tags
76+
$content = preg_replace("@=\\s*[\"']\\s*(<x-trans.+<\\/x-trans>)\\s*[\"']@mi", "=\"🚫 Can't be translated here. 🚫\"", $content);
77+
$content = preg_replace('@&lt;x-trans.+data-key=&quot;([^&]+)&quot;.+&lt;\\/x-trans&gt;@mi', '🚫 $1 🚫', $content);
78+
79+
$html = sprintf(
80+
self::HTML,
81+
$this->packages->getUrl('bundles/translation/css/content-tools.min.css'),
82+
$this->packages->getUrl('bundles/translation/js/content-tools.min.js'),
83+
$this->packages->getUrl('bundles/translation/js/editInPlace.js'),
84+
85+
$this->router->generate('translation_edit_in_place_update', [
86+
'configName' => $this->configName,
87+
'locale' => $event->getRequest()->getLocale(),
88+
])
89+
);
90+
$content = str_replace('</body>', $html."\n".'</body>', $content);
91+
92+
$response = $event->getResponse();
93+
94+
// Remove the cache because we do not want the modified page to be cached
95+
$response->headers->set('cache-control', 'no-cache, no-store, must-revalidate');
96+
$response->headers->set('pragma', 'no-cache');
97+
$response->headers->set('expires', '0');
98+
99+
$event->getResponse()->setContent($content);
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)