Skip to content

Commit

Permalink
feature #2766 Added a "code" form type (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was merged into the 2.0.x-dev branch.

Discussion
----------

Added a "code" form type

This adds a custom form type for new/edit pages based on the popular CodeMirror code editor (I tested Microsoft's Monaco editor, Ace editor, etc. ... none worked as great as CodeMirror).

You only need to add `type: 'code'` in your form property and ... boom 💥  you have a source code editor with syntax highlighting, etc. This is how it looks with more than one editor in the same page:

![image](https://user-images.githubusercontent.com/73419/59554577-6ea20b00-8fa5-11e9-9780-b566b35131b0.png)

@yceruto I guess what I did in `EasyAdminCodeTypeConfigurator` is "wrong" 🙈 and the type/value validation should be moved to the form type. Can you give me a clue about that? Thanks!

Commits
-------

caa3a36 Added a "code editor" form type
  • Loading branch information
javiereguiluz committed Jun 26, 2019
2 parents 151b83c + caa3a36 commit de1128f
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 5 deletions.
5 changes: 5 additions & 0 deletions assets/css/easyadmin-theme/forms.scss
Expand Up @@ -468,3 +468,8 @@ form .invalid-feedback > .d-block + .d-block {
text-align: right;
top: 0;
}

// EasyAdminCodeType
.field-easyadmin_code_editor .form-widget {
flex-basis: 65%;
}
24 changes: 24 additions & 0 deletions assets/css/form-type-code-editor.css
@@ -0,0 +1,24 @@
@import '~codemirror/lib/codemirror.css';

.CodeMirror {
font: 13px/1.5 SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Ubuntu Mono", "Courier New", monospace;
height: auto;
min-height: 45px;
}

.CodeMirror-wrap {
box-shadow: 0 0 0 1px rgba(43, 45, 80, .16), 0 0 0 1px rgba(6, 122, 184, 0), 0 0 0 2px rgba(6, 122, 184, 0), 0 1px 1px rgba(0, 0, 0, .08);;
border-radius: var(--border-radius);
}

.CodeMirror-gutters {
background: var(--gray-100);
}

.CodeMirror-linenumber {
color: var(--text-muted);
}

.CodeMirror-lines {
padding-bottom: 5px;
}
21 changes: 21 additions & 0 deletions assets/js/app.js
Expand Up @@ -28,6 +28,7 @@ window.addEventListener('load', function() {
$(document).on('easyadmin.collection.item-added', createAutoCompleteFields);
createContentResizer();
createNavigationToggler();
createCodeEditorFields();
});

function createNullableControls() {
Expand Down Expand Up @@ -138,3 +139,23 @@ function createNavigationToggler() {
}
});
}

// Code editor fields require extra JavaScript dependencies, which are loaded
// dynamically only when there are code editor fields in the page
function createCodeEditorFields()
{
const codeEditorElements = document.querySelectorAll('[data-easyadmin-code-editor]');
if (codeEditorElements.length === 0) {
return;
}

const codeEditorJs = document.createElement('script');
codeEditorJs.setAttribute('src', codeEditorElements[0].dataset.jsUrl);

const codeEditorCss = document.createElement('link');
codeEditorCss.setAttribute('rel', 'stylesheet');
codeEditorCss.setAttribute('href', codeEditorElements[0].dataset.cssUrl);

document.querySelector('head').appendChild(codeEditorCss);
document.querySelector('body').appendChild(codeEditorJs);
}
31 changes: 31 additions & 0 deletions assets/js/form-type-code-editor.js
@@ -0,0 +1,31 @@
require('../css/form-type-code-editor.css');

import CodeMirror from 'codemirror';

import 'codemirror/mode/css/css';
import 'codemirror/mode/dockerfile/dockerfile';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/nginx/nginx';
import 'codemirror/mode/php/php';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/sql/sql';
import 'codemirror/mode/twig/twig';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter';
import 'codemirror/mode/yaml/yaml';

document.querySelectorAll('[data-easyadmin-code-editor]').forEach(function(codeBlock) {
CodeMirror.fromTextArea(codeBlock, {
autocapitalize: false,
autocorrect: false,
indentWithTabs: codeBlock.dataset.indentWithTabs,
lineNumbers: true,
lineWrapping: true,
mode: codeBlock.dataset.language,
scrollbarStyle: 'native',
spellcheck: false,
tabSize: codeBlock.dataset.tabSize,
theme: 'default',
});
});
54 changes: 52 additions & 2 deletions doc/book/edit-new-configuration.rst
Expand Up @@ -384,8 +384,8 @@ These are the options that you can define for each field:
done internally by the bundle). The allowed values are:

* Any of the `Symfony Form types`_.
* Any of the custom EasyAdmin form types: ``easyadmin_autocomplete`` (they are
explained later in this chapter).
* Any of the custom EasyAdmin form types: ``code_editor``, ``easyadmin_autocomplete``
(they are explained later in this chapter).
* ``type_options`` (optional), a hash with the options passed to the Symfony
Form type used to render the field.

Expand Down Expand Up @@ -555,6 +555,56 @@ change this value (globally or per entity):
max_results: 5
# ...
Code Editor
-----------

It displays a JavaScript-based editor for source code. It provides advanced
features such as code highlighting and smart indenting.

.. code-block:: yaml
# config/packages/easy_admin.yaml
easy_admin:
entities:
Server:
class: App\Entity\Server
form:
fields:
- { property: 'config', type: 'code_editor', language: 'nginx' }
# ...
# ...
This type defines the following configuration options:

* ``height``: the initial height of code blocks is the same as their contents
and it grows automatically as you add more contents. This option, which must
be an integer, sets the height of the code block element in pixels. If
contents don't fit, a scrollbar is displayed.
* ``language``: sets the programming language used for the syntax highlighting
of the code (the language can't be autodetected from the contents). The available
languages are: ``css``, ``dockerfile``, ``js`` (equivalent to ``javascript``),
``markdown``, ``nginx``, ``php``, ``shell``, ``sql``, ``twig``, ``xml``,
``yaml-frontmatter`` (used in some blogs, CMS systems and static site
generators), ``yaml``.
* ``tab_size``: an integer (default: ``4``) that defines the indention size (no
matter if the code uses white spaces or tabs).
* ``indent_with_tabs``: if this boolean option is set to ``true``, code is
indented with real tabs instead of white spaces (default: ``false``).

.. code-block:: yaml
# config/packages/easy_admin.yaml
easy_admin:
entities:
ExamQuestion:
class: App\Entity\ExamQuestion
form:
fields:
- { property: 'question', type: 'code_editor', language: 'yaml', height: 150, tab_size: 4 }
- { property: 'codeSample', type: 'code_editor', language: 'php', height: 600, tab_size: 2 }
# ...
# ...
.. _edit-new-advanced-form-design:

Advanced Form Design
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -4,6 +4,7 @@
"@symfony/webpack-encore": "^0.21",
"bootstrap": "^4.1.0",
"bootstrap-rtl": "^3.3.4",
"codemirror": "^5.47.0",
"cssnano": "^4.1.7",
"featherlight": "^1.7.13",
"jquery": "^3.3.1",
Expand Down
72 changes: 72 additions & 0 deletions src/Form/Type/CodeEditorType.php
@@ -0,0 +1,72 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class CodeEditorType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['height'] = $options['height'];
$view->vars['tabSize'] = $options['tab_size'];
$view->vars['indentWithTabs'] = $options['indent_with_tabs'];
$view->vars['language'] = $options['language'];
}

/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'height' => null,
'tab_size' => 4,
'indent_with_tabs' => false,
// the code editor can't autodetect the language, so 'markdown' is used when
// no language is selected explicitly (because it's the most similar to regular text)
'language' => 'markdown',
]);
$resolver->setAllowedTypes('height', ['null', 'int']);
$resolver->setAllowedTypes('tab_size', 'int');
$resolver->setAllowedTypes('indent_with_tabs', 'bool');
$resolver->setAllowedTypes('language', 'string');
$resolver->setAllowedValues('language', ['css', 'dockerfile', 'js', 'javascript', 'markdown', 'nginx', 'php', 'shell', 'sql', 'twig', 'xml', 'yaml-frontmatter', 'yaml']);

// define some programming language shortcuts for better UX (e.g. 'js' === 'javascript')
$resolver->setNormalizer('language', static function (Options $options, $language) {
if ('js' === $language) {
$language = 'javascript';
}

return $language;
});
}

/**
* {@inheritdoc}
*/
public function getParent(): string
{
return TextareaType::class;
}

/**
* {@inheritdoc}
*/
public function getBlockPrefix(): string
{
return 'easyadmin_code_editor';
}
}
43 changes: 43 additions & 0 deletions src/Form/Type/Configurator/CodeEditorTypeConfigurator.php
@@ -0,0 +1,43 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator;

use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CodeEditorType;
use Symfony\Component\Form\FormConfigInterface;

/**
* This configurator is applied to any form field of type 'code_editor'.
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class CodeEditorTypeConfigurator implements TypeConfiguratorInterface
{
/**
* {@inheritdoc}
*/
public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig): array
{
if (isset($metadata['height'])) {
$options['height'] = $metadata['height'];
}
if (isset($metadata['tab_size'])) {
$options['tab_size'] = $metadata['tab_size'];
}
if (isset($metadata['indent_with_tabs'])) {
$options['indent_with_tabs'] = $metadata['indent_with_tabs'];
}
if (isset($metadata['language'])) {
$options['language'] = $metadata['language'];
}

return $options;
}

/**
* {@inheritdoc}
*/
public function supports($type, array $options, array $metadata): bool
{
return \in_array($type, ['code_editor', CodeEditorType::class], true);
}
}
4 changes: 4 additions & 0 deletions src/Form/Util/FormTypeHelper.php
Expand Up @@ -2,6 +2,7 @@

namespace EasyCorp\Bundle\EasyAdminBundle\Form\Util;

use EasyCorp\Bundle\EasyAdminBundle\Form\Type\CodeEditorType;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminAutocompleteType;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminDividerType;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminFormType;
Expand Down Expand Up @@ -59,6 +60,9 @@ final class FormTypeHelper
'button' => ButtonType::class,
'checkbox' => CheckboxType::class,
'choice' => ChoiceType::class,
// allow using underscore and dashes to improve DX
'code-editor' => CodeEditorType::class,
'code_editor' => CodeEditorType::class,
'collection' => CollectionType::class,
'color' => ColorType::class,
'country' => CountryType::class,
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/form.xml
Expand Up @@ -45,6 +45,11 @@

<!-- Type Configurators -->

<service id="easyadmin.form.type.configurator.code_editor" public="false"
class="EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\CodeEditorTypeConfigurator">
<tag name="easyadmin.form.type.configurator" priority="50" />
</service>

<service id="easyadmin.form.type.configurator.textarea" public="false"
class="EasyCorp\Bundle\EasyAdminBundle\Form\Type\Configurator\TextareaTypeConfigurator">
<tag name="easyadmin.form.type.configurator" priority="40" />
Expand Down
2 changes: 1 addition & 1 deletion src/Resources/public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Resources/public/app.css.map

Large diffs are not rendered by default.

0 comments on commit de1128f

Please sign in to comment.