diff --git a/config/app_local.example.php b/config/app_local.example.php index 7e7bd1ccc..2f2cc1588 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -564,16 +564,6 @@ // ], // ], - /** - * UI settings. - * index.copy2clipboard => enable "onmouseover" of index general cells showing copy to clipboard button - */ - // 'UI' => [ - // 'index' => [ - // 'copy2clipboard' => true, - // ], - // ], - /** * Richeditor configuration. */ @@ -618,6 +608,7 @@ * UI settings. * - index: index settings. 'copy2clipboard' enables "onmouseover" of index general cells showing copy to clipboard button * - modules: modules settings. 'counters' to show counters in modules; 'all', 'none', to show all, none or custom modules. Default is ['trash'] + * - richeditor: richeditor settings per field: you can set 'config' and 'toolbar' per single field. */ // 'UI' => [ // 'index' => [ @@ -626,6 +617,34 @@ // 'modules' => [ // 'counters' => ['objects', 'media', 'images', 'videos', 'audio', 'files', 'trash', 'users'], // ], + // 'richeditor' => [ + // 'title' => [ + // 'config' => [ + // 'forced_root_block' => 'div', + // 'forced_root_block_attrs' => ['class' => 'titleContainer'], + // ], + // 'toolbar' => [ + // 'italic', + // 'subscript', + // 'superscript', + // ], + // ], + // 'description' => [ + // 'config' => [ + // 'forced_root_block' => 'div', + // 'forced_root_block_attrs' => ['class' => 'descriptionContainer'], + // ], + // 'toolbar' => [ + // 'bold', + // 'italic', + // 'subscript', + // 'superscript', + // 'link', + // 'unlink', + // 'code', + // ], + // ], + // ], // ], /** diff --git a/resources/js/app/components/index-cell/index-cell.js b/resources/js/app/components/index-cell/index-cell.js deleted file mode 100644 index 639237be1..000000000 --- a/resources/js/app/components/index-cell/index-cell.js +++ /dev/null @@ -1,70 +0,0 @@ -import { t } from 'ttag'; - -export default { - name: 'index-cell', - - template: ` -
- <: !msg ? truncated : '' :> - -
-
- `, - - props: { - settings: {}, - prop: '', - text: '', - untitledlabel: '', - }, - - data() { - return { - msg: '', - showCopy: false, - truncated: '', - }; - }, - - async mounted() { - this.truncated = this.text.length <= 100 ? this.text : this.text.substring(0, 100); - }, - - methods: { - className() { - return `${this.prop}-cell`; - }, - - copy() { - navigator.clipboard.writeText(this.text); - this.msg = t`copied in the clipboard`; - setTimeout(() => this.reset(), 2000) - }, - - onMouseover() { - if (this.settings?.copy2clipboard !== true) { - return; - } - this.showCopy = true; - }, - - onMouseleave() { - if (this.settings?.copy2clipboard !== true) { - return; - } - this.showCopy = false; - }, - - reset() { - this.msg = ''; - }, - - showCopyIcon() { - if (this.settings?.copy2clipboard !== true) { - return false; - } - - return !this.msg && this.showCopy; - }, - }, -}; diff --git a/resources/js/app/components/index-cell/index-cell.vue b/resources/js/app/components/index-cell/index-cell.vue new file mode 100644 index 000000000..b86b5a4ba --- /dev/null +++ b/resources/js/app/components/index-cell/index-cell.vue @@ -0,0 +1,104 @@ + + + diff --git a/resources/js/app/components/recent-activity/recent-activity.vue b/resources/js/app/components/recent-activity/recent-activity.vue index 216a3583e..ffea9851b 100644 --- a/resources/js/app/components/recent-activity/recent-activity.vue +++ b/resources/js/app/components/recent-activity/recent-activity.vue @@ -171,12 +171,20 @@ export default { this.loading = false; } }, + getTextFromHtml(s) { + const e = document.createElement('div'); + e.innerHTML = s; + + return e.textContent || e.innerText || ''; + }, pageButtonClass() { return 'has-text-size-smallest button is-width-auto button-outlined'; }, title(item) { if (item.object_title) { - return this.$helpers.truncate(item.object_title, 100); + const text = this.getTextFromHtml(item.object_title); + + return this.$helpers.truncate(text, 100); } const id = `#${item.meta.resource_id}`; const uname = item.object_uname ? this.$helpers.truncate(item.object_uname, 100) : t`(deleted)`; diff --git a/resources/js/app/directives/richeditor.js b/resources/js/app/directives/richeditor.js index 22f87259f..b56274489 100644 --- a/resources/js/app/directives/richeditor.js +++ b/resources/js/app/directives/richeditor.js @@ -79,11 +79,9 @@ export default { */ async inserted(element, binding) { let changing = false; - let items = JSON.parse(binding.expression || ''); - if (!items) { - items = DEFAULT_TOOLBAR; - } - + const exp = binding.expression || ''; + const json = JSON.parse(exp); + let items = json ? json.join(' ') : DEFAULT_TOOLBAR; if (!binding.modifiers?.placeholders) { items = items.replace(/\bplaceholders\b/, ''); } @@ -121,10 +119,20 @@ export default { add_unload_trigger: false, // fix populating textarea elements with garbage when the user initiates a navigation with unsaved changes, but cancels it when the alert is shown readonly: element.getAttribute('readonly') === 'readonly' ? 1 : 0, ... BEDITA?.richeditorConfig, + ... BEDITA?.richeditorByPropertyConfig?.[element?.name]?.config || {}, setup: (editor) => { editor.on('change', () => { EventBus.send('refresh-placeholders', {id: editor.id, content: editor.getContent()}); }); + editor.on('init', () => { + // force height from config + const height = BEDITA?.richeditorByPropertyConfig?.[element?.name]?.config?.height; + if (height) { + const id = editor.id; + const elem = document.getElementById(id + '_ifr').parentNode.parentNode.parentNode.parentNode; + elem.style.height = height; + } + }); } }); diff --git a/src/Form/Control.php b/src/Form/Control.php index 7f160b4f3..8268b2e18 100644 --- a/src/Form/Control.php +++ b/src/Form/Control.php @@ -149,11 +149,13 @@ public static function richtext(array $options): array { $schema = (array)Hash::get($options, 'schema'); $value = Hash::get($options, 'value'); - $key = !empty($schema['placeholders']) ? 'v-richeditor.placeholders' : 'v-richeditor'; + $richeditorKey = !empty($schema['placeholders']) ? 'v-richeditor.placeholders' : 'v-richeditor'; + $override = !empty($options[$richeditorKey]); + $toolbar = $override ? $options[$richeditorKey] : json_encode(Configure::read('RichTextEditor.default.toolbar', '')); return [ 'type' => 'textarea', - $key => json_encode(Configure::read('RichTextEditor.default.toolbar', '')), + $richeditorKey => $toolbar, 'value' => $value, ]; } diff --git a/src/View/Helper/LayoutHelper.php b/src/View/Helper/LayoutHelper.php index a5b884846..9ba4cf096 100644 --- a/src/View/Helper/LayoutHelper.php +++ b/src/View/Helper/LayoutHelper.php @@ -109,6 +109,7 @@ public function title(): string $name = (string)Hash::get($module, 'name'); $object = (array)$this->getView()->get('object'); $title = (string)Hash::get($object, 'attributes.title'); + $title = strip_tags($title); return empty($title) ? $name : sprintf('%s | %s', $title, $name); } @@ -371,6 +372,7 @@ public function metaConfig(): array 'uploadConfig' => $this->System->uploadConfig(), 'relationsSchema' => $this->getView()->get('relationsSchema', []), 'richeditorConfig' => (array)Configure::read('Richeditor'), + 'richeditorByPropertyConfig' => (array)Configure::read('UI.richeditor', []), ]; } diff --git a/src/View/Helper/SchemaHelper.php b/src/View/Helper/SchemaHelper.php index aa3274488..43dd4bd4a 100644 --- a/src/View/Helper/SchemaHelper.php +++ b/src/View/Helper/SchemaHelper.php @@ -66,6 +66,8 @@ public function controlOptions(string $name, $value, ?array $schema = null): arr $ctrlOptions = (array)Configure::read($ctrlOptionsPath); if (!empty($options)) { + $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $options); + return array_merge($options, [ 'label' => Hash::get($ctrlOptions, 'label'), 'readonly' => Hash::get($ctrlOptions, 'readonly', false), @@ -98,10 +100,30 @@ public function controlOptions(string $name, $value, ?array $schema = null): arr if (!empty($ctrlOptions['step'])) { $opts['step'] = $ctrlOptions['step']; } + $this->updateRicheditorOptions($name, !empty($schema['placeholders']), $opts); return Control::control($opts); } + /** + * Update richeditor options if a toolbar config is defined in UI.richeditor for the property. + * + * @param string $name Property name + * @param bool $placeholders True if property has placeholders in schema + * @param array $options Control options + * @return void + */ + protected function updateRicheditorOptions(string $name, bool $placeholders, array &$options) + { + $uiRichtext = (array)Configure::read(sprintf('UI.richeditor.%s.toolbar', $name)); + if (empty($uiRichtext)) { + return; + } + $options['type'] = 'textarea'; + $richeditorKey = $placeholders ? 'v-richeditor.placeholders' : 'v-richeditor'; + $options[$richeditorKey] = json_encode($uiRichtext); + } + /** * Return custom control array if a custom handler has been defined or null otherwise. * diff --git a/templates/Element/Modules/index_table_row.twig b/templates/Element/Modules/index_table_row.twig index 9d18f16bb..5e881c658 100644 --- a/templates/Element/Modules/index_table_row.twig +++ b/templates/Element/Modules/index_table_row.twig @@ -22,7 +22,7 @@ {{ element('Modules/index_properties_date_ranges', { dateRanges: object.attributes[prop] }) }} {% else %} - + {% endif %} {% endfor %} diff --git a/templates/Layout/default.twig b/templates/Layout/default.twig index e2b5e9b6d..f76b35823 100644 --- a/templates/Layout/default.twig +++ b/templates/Layout/default.twig @@ -19,7 +19,7 @@ BEdita Manager {{ Html.meta('description', 'BEdita Manager, official BEdita API admin tool')|raw }} {{ Html.meta('generator', 'BEdita Manager v' ~ config('Manager.version'))|raw }} - {{ "#{_view.fetch('title')} | #{project.name ?: 'BEdita Manager'}" }} + {{ Layout.title()|default(_view.fetch('title')) }} | {{ "#{project.name ?: 'BEdita Manager'}" }} {{ Html.meta('icon', "favicon.png", { type:'image/png' } )|raw }} {# fonts #} diff --git a/templates/Layout/error.twig b/templates/Layout/error.twig index fec971d22..4cc7a2fe6 100644 --- a/templates/Layout/error.twig +++ b/templates/Layout/error.twig @@ -29,7 +29,7 @@ BEdita Manager {{ element('custom_colors') }} - {{ "#{_view.fetch('title')} | #{project.name ?: 'BEdita Manager'}" }} + {{ Layout.title()|default(_view.fetch('title')) }} | {{ "#{project.name ?: 'BEdita Manager'}" }} {{ element('json_meta_config') }} diff --git a/templates/Pages/Modules/view.twig b/templates/Pages/Modules/view.twig index 6c93d24b7..00e44a989 100644 --- a/templates/Pages/Modules/view.twig +++ b/templates/Pages/Modules/view.twig @@ -4,15 +4,17 @@
{% if object.id %} -

{{ object.attributes.title }} - {% for obj in included %} - {% if obj.type == 'streams' %} - - - {{ __('Open File') }} - - {% endif %} - {% endfor %}

+

+ + {% for obj in included %} + {% if obj.type == 'streams' %} + + + {{ __('Open File') }} + + {% endif %} + {% endfor %} +

{% else %}

{{ __('New object in') }} {{ Layout.tr(object.type) }}

{% endif %} diff --git a/tests/TestCase/View/Helper/LayoutHelperTest.php b/tests/TestCase/View/Helper/LayoutHelperTest.php index d8f0dd4ed..1eea1fddc 100644 --- a/tests/TestCase/View/Helper/LayoutHelperTest.php +++ b/tests/TestCase/View/Helper/LayoutHelperTest.php @@ -457,6 +457,18 @@ public function titleProvider(): array 'currentModule' => ['name' => 'video'], ], ], + 'video html' => [ + 'Video title | video', + 'Module', + [ + 'object' => [ + 'attributes' => [ + 'title' => '
Video title
', + ], + ], + 'currentModule' => ['name' => 'video'], + ], + ], ]; } @@ -584,6 +596,7 @@ public function testMetaConfig(): void 'uploadConfig' => $system->uploadConfig(), 'relationsSchema' => ['whatever'], 'richeditorConfig' => (array)Configure::read('Richeditor'), + 'richeditorByPropertyConfig' => (array)Configure::read('RicheditorByProperty'), ]; static::assertSame($expected, $conf); } diff --git a/tests/TestCase/View/Helper/SchemaHelperTest.php b/tests/TestCase/View/Helper/SchemaHelperTest.php index 2289cd22a..b3e81a735 100644 --- a/tests/TestCase/View/Helper/SchemaHelperTest.php +++ b/tests/TestCase/View/Helper/SchemaHelperTest.php @@ -409,6 +409,96 @@ public function testControlOptions(array $expected, array $schema, string $name, static::assertEquals(sort($expected), sort($actual)); } + /** + * Data provider for `testUpdateRicheditorOptions` test case. + * + * @return array + */ + public function updateRicheditorOptionsProvider(): array + { + return [ + 'empty UI.richeditor.title.toolbar' => [ + [], + 'title', + false, + [], + [], + ], + 'empty UI.richeditor.title.toolbar and placeholders' => [ + [], + 'title', + true, + [], + [], + ], + 'not empty UI.richeditor.title.toolbar' => [ + [ + 'italic', + 'subscript', + 'superscript', + ], + 'title', + false, + [], + [ + 'type' => 'textarea', + 'v-richeditor' => '["italic","subscript","superscript"]', + ], + ], + 'not empty UI.richeditor.title.toolbar and placeholders' => [ + [ + 'italic', + 'subscript', + 'superscript', + ], + 'title', + true, + [], + [ + 'type' => 'textarea', + 'v-richeditor.placeholders' => '["italic","subscript","superscript"]', + ], + ], + ]; + } + + /** + * Test `updateRicheditorOptions` method. + * + * @param array $uiConf The UI configuration + * @param string $name The field name + * @param bool $placeholders Use placeholders + * @param array $options The options + * @param array $expected The expected result + * @return void + * @dataProvider updateRicheditorOptionsProvider() + * @covers ::updateRicheditorOptions() + */ + public function testUpdateRicheditorOptions(array $uiConf, string $name, bool $placeholders, array $options, array $expected): void + { + Configure::write('UI.richeditor.title.toolbar', $uiConf); + $request = new ServerRequest([ + 'environment' => [ + 'REQUEST_METHOD' => 'GET', + ], + 'params' => [ + 'object_type' => 'dummies', + ], + ]); + $view = new View($request, null, null, []); + $view->set('objectType', 'dummies'); + $helper = new class ($view) extends SchemaHelper { + public function updateREopts(string $name, bool $placeholders, array &$options): array + { + $this->updateRicheditorOptions($name, $placeholders, $options); + + return $options; + } + }; + $actual = $helper->updateREopts($name, $placeholders, $options); + static::assertEquals($expected, $actual); + } + /** * Test `lang` property *