diff --git a/.idea/php.xml b/.idea/php.xml index cad75414..b37ec3bb 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -118,6 +118,7 @@ + diff --git a/composer.json b/composer.json index a24ada54..db42c77c 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "php": "^7.1.3", "ext-ctype": "*", "ext-iconv": "*", + "friendsofsymfony/ckeditor-bundle": "^2.0", "omines/datatables-bundle": "^0.2.2", "sensio/framework-extra-bundle": "^5.1", "shivas/versioning-bundle": "^3.1", diff --git a/composer.lock b/composer.lock index 8e347502..67b3807e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "b8930537cde9ee459d093b68931b8d5a", + "content-hash": "9226477561eef66d140fe1e9a6ccd4a1", "packages": [ { "name": "doctrine/annotations", @@ -1301,6 +1301,84 @@ ], "time": "2016-10-17T18:31:11+00:00" }, + { + "name": "friendsofsymfony/ckeditor-bundle", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle.git", + "reference": "ca2b528d9a9939ca068fa01f0cddbce6cebcff13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/ca2b528d9a9939ca068fa01f0cddbce6cebcff13", + "reference": "ca2b528d9a9939ca068fa01f0cddbce6cebcff13", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "php": "^7.1", + "symfony/asset": "^3.4 || ^4.0", + "symfony/config": "^3.4 || ^4.0", + "symfony/dependency-injection": "^3.4 || ^4.0", + "symfony/expression-language": "^3.4 || ^4.0", + "symfony/form": "^3.4 || ^4.0", + "symfony/framework-bundle": "^3.4 || ^4.0", + "symfony/http-foundation": "^3.4 || ^4.0", + "symfony/http-kernel": "^3.4 || ^4.0", + "symfony/options-resolver": "^3.4 || ^4.0", + "symfony/property-access": "^3.4 || ^4.0", + "symfony/routing": "^3.4 || ^4.0", + "symfony/twig-bundle": "^3.4 || ^4.0", + "twig/twig": "^2.0" + }, + "conflict": { + "sebastian/environment": "<1.3.4", + "sebastian/exporter": "<2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "matthiasnoback/symfony-dependency-injection-test": "^1.0 || ^2.0", + "phpunit/phpunit": "^6.0", + "symfony/console": "^3.4 || ^4.0", + "symfony/phpunit-bridge": "^4.1", + "symfony/yaml": "^3.4 || ^4.0" + }, + "suggest": { + "egeloen/form-extra-bundle": "Allows to load CKEditor asynchronously" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\CKEditorBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/graphs/contributors" + } + ], + "description": "Provides a CKEditor integration for your Symfony project.", + "keywords": [ + "CKEditor" + ], + "time": "2019-03-05T21:04:46+00:00" + }, { "name": "jdorn/sql-formatter", "version": "v1.2.17", diff --git a/config/bundles.php b/config/bundles.php index 36851112..8b2eb20d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -17,4 +17,5 @@ return [ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Omines\DataTablesBundle\DataTablesBundle::class => ['all' => true], Shivas\VersioningBundle\ShivasVersioningBundle::class => ['all' => true], + FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], ]; diff --git a/config/packages/fos_ckeditor.yaml b/config/packages/fos_ckeditor.yaml new file mode 100644 index 00000000..c52b604d --- /dev/null +++ b/config/packages/fos_ckeditor.yaml @@ -0,0 +1,21 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/FOSCKEditorBundle/index.html + +twig: + form_themes: + - '@FOSCKEditor/Form/ckeditor_widget.html.twig' + +fos_ck_editor: + default_config: complex_config + configs: + complex_config: + extraPlugins: "bbcode" + toolbar: standard + simple_config: + extraPlugins: "bbcode" + toolbar: basic + + + plugins: + bbcode: + path: "/ckeditor/plugins/bbcode/" # with trailing slash + filename: "plugin.js" \ No newline at end of file diff --git a/package.json b/package.json index eec381f9..a3302a33 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build": "encore production --progress" }, "dependencies": { + "@ckeditor/ckeditor5-build-classic": "^12.0.0", "datatables.net-bs4": "^1.10.19", "datatables.net-buttons-bs4": "^1.5.4", "datatables.net-fixedheader-bs4": "^3.1.5", diff --git a/public/ckeditor/plugins/bbcode/dev/bbcode.html b/public/ckeditor/plugins/bbcode/dev/bbcode.html new file mode 100644 index 00000000..bbfb1bca --- /dev/null +++ b/public/ckeditor/plugins/bbcode/dev/bbcode.html @@ -0,0 +1,168 @@ + + + + + + BBCode plugin playground – CKEditor Sample + + + + + +

+ CKEditor Sample — BBCode plugin playground +

+
+
+

+ This sample shows how to configure CKEditor to output BBCode format instead of HTML. + Please note that the editor configuration was modified to reflect what is needed in a BBCode editing environment. + Smiley images, for example, were stripped to the emoticons that are commonly used in some BBCode dialects. +

+

+ Please note that currently there is no standard for the BBCode markup language, so its implementation + for different platforms (message boards, blogs etc.) can vary. This means that before using CKEditor to + output BBCode you may need to adjust the implementation to your own environment. +

+

+ A snippet of the configuration code can be seen below; check the source of this page for + a full definition: +

+
+CKEDITOR.inline( 'editor1', {
+	extraPlugins : 'bbcode',
+	(below configurations details omitted:)
+	toolbar : ...,
+	fontSize_sizes : ...,
+	smiley_images : ...,
+	smiley_descriptions : ...
+});
+
+ +
+ +
+
+

BBCode Output:

+
+		
+
+
+ + + + diff --git a/public/ckeditor/plugins/bbcode/plugin.js b/public/ckeditor/plugins/bbcode/plugin.js new file mode 100644 index 00000000..dcc468f1 --- /dev/null +++ b/public/ckeditor/plugins/bbcode/plugin.js @@ -0,0 +1,809 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +( function() { + CKEDITOR.on( 'dialogDefinition', function( ev ) { + var tab, + name = ev.data.name, + definition = ev.data.definition; + + if ( name == 'link' ) { + definition.removeContents( 'target' ); + definition.removeContents( 'upload' ); + definition.removeContents( 'advanced' ); + tab = definition.getContents( 'info' ); + tab.remove( 'emailSubject' ); + tab.remove( 'emailBody' ); + } else if ( name == 'image' ) { + definition.removeContents( 'advanced' ); + tab = definition.getContents( 'Link' ); + tab.remove( 'cmbTarget' ); + tab = definition.getContents( 'info' ); + tab.remove( 'txtAlt' ); + tab.remove( 'basic' ); + } + } ); + + var bbcodeMap = { b: 'strong', u: 'u', i: 'em', color: 'span', size: 'span', left: 'div', right: 'div', center: 'div', justify: 'div', quote: 'blockquote', code: 'code', url: 'a', email: 'span', img: 'span', '*': 'li', list: 'ol' }, + convertMap = { strong: 'b', b: 'b', u: 'u', em: 'i', i: 'i', code: 'code', li: '*' }, + tagnameMap = { strong: 'b', em: 'i', u: 'u', li: '*', ul: 'list', ol: 'list', code: 'code', a: 'link', img: 'img', blockquote: 'quote' }, + stylesMap = { color: 'color', size: 'font-size', left: 'text-align', center: 'text-align', right: 'text-align', justify: 'text-align' }, + attributesMap = { url: 'href', email: 'mailhref', quote: 'cite', list: 'listType' }; + + // List of block-like tags. + var dtd = CKEDITOR.dtd, + blockLikeTags = CKEDITOR.tools.extend( { table: 1 }, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list ); + + var semicolonFixRegex = /\s*(?:;\s*|$)/; + + function serializeStyleText( stylesObject ) { + var styleText = ''; + for ( var style in stylesObject ) { + var styleVal = stylesObject[ style ], + text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); + + styleText += text; + } + return styleText; + } + + // Maintain the map of smiley-to-description. + // jscs:disable maximumLineLength + var smileyMap = { smiley: ':)', sad: ':(', wink: ';)', laugh: ':D', cheeky: ':P', blush: ':*)', surprise: ':-o', indecision: ':|', angry: '>:(', angel: 'o:)', cool: '8-)', devil: '>:-)', crying: ';(', kiss: ':-*' }, + // jscs:enable maximumLineLength + smileyReverseMap = {}, + smileyRegExp = []; + + // Build regexp for the list of smiley text. + for ( var i in smileyMap ) { + smileyReverseMap[ smileyMap[ i ] ] = i; + smileyRegExp.push( smileyMap[ i ].replace( /\(|\)|\:|\/|\*|\-|\|/g, function( match ) { + return '\\' + match; + } ) ); + } + + smileyRegExp = new RegExp( smileyRegExp.join( '|' ), 'g' ); + + var decodeHtml = ( function() { + var regex = [], + entities = { + nbsp: '\u00A0', // IE | FF + shy: '\u00AD' // IE + }; + + for ( var entity in entities ) + regex.push( entity ); + + regex = new RegExp( '&(' + regex.join( '|' ) + ');', 'g' ); + + return function( html ) { + return html.replace( regex, function( match, entity ) { + return entities[ entity ]; + } ); + }; + } )(); + + CKEDITOR.BBCodeParser = function() { + this._ = { + bbcPartsRegex: /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig + }; + }; + + CKEDITOR.BBCodeParser.prototype = { + parse: function( bbcode ) { + var parts, part, + lastIndex = 0; + + while ( ( parts = this._.bbcPartsRegex.exec( bbcode ) ) ) { + var tagIndex = parts.index; + if ( tagIndex > lastIndex ) { + var text = bbcode.substring( lastIndex, tagIndex ); + this.onText( text, 1 ); + } + + lastIndex = this._.bbcPartsRegex.lastIndex; + + // "parts" is an array with the following items: + // 0 : The entire match for opening/closing tags and line-break; + // 1 : line-break; + // 2 : open of tag excludes option; + // 3 : tag option; + // 4 : close of tag; + + part = ( parts[ 1 ] || parts[ 3 ] || '' ).toLowerCase(); + // Unrecognized tags should be delivered as a simple text (https://dev.ckeditor.com/ticket/7860). + if ( part && !bbcodeMap[ part ] ) { + this.onText( parts[ 0 ] ); + continue; + } + + // Opening tag + if ( parts[ 1 ] ) { + var tagName = bbcodeMap[ part ], + attribs = {}, + styles = {}, + optionPart = parts[ 2 ]; + + // Special handling of justify tags, these provide the alignment as a tag name (#2248). + if ( part == 'left' || part == 'right' || part == 'center' || part == 'justify' ) { + optionPart = part; + } + + if ( optionPart ) { + if ( part == 'list' ) { + if ( !isNaN( optionPart ) ) + optionPart = 'decimal'; + else if ( /^[a-z]+$/.test( optionPart ) ) + optionPart = 'lower-alpha'; + else if ( /^[A-Z]+$/.test( optionPart ) ) + optionPart = 'upper-alpha'; + } + + if ( stylesMap[ part ] ) { + // Font size represents percentage. + if ( part == 'size' ) { + optionPart += '%'; + } + + styles[ stylesMap[ part ] ] = optionPart; + attribs.style = serializeStyleText( styles ); + } else if ( attributesMap[ part ] ) { + // All the input BBCode is encoded at the beginning so <> characters in the textual part + // are later correctly preserved in HTML. However... it affects parts that now become + // attributes, so we need to revert that. As a matter of fact, the content should not be + // encoded at the beginning, but only later when creating text nodes (encoding should be more precise) + // but it's too late not for such changes. + attribs[ attributesMap[ part ] ] = CKEDITOR.tools.htmlDecode( optionPart ); + } + } + + // Two special handling - image and email, protect them + // as "span" with an attribute marker. + if ( part == 'email' || part == 'img' ) + attribs.bbcode = part; + + this.onTagOpen( tagName, attribs, CKEDITOR.dtd.$empty[ tagName ] ); + } + // Closing tag + else if ( parts[ 3 ] ) { + this.onTagClose( bbcodeMap[part] ); + } + } + + if ( bbcode.length > lastIndex ) + this.onText( bbcode.substring( lastIndex, bbcode.length ), 1 ); + } + }; + + /** + * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. + * + * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' ); + * alert( fragment.children[ 0 ].name ); // 'b' + * alert( fragment.children[ 1 ].value ); // ' Text' + * + * @static + * @member CKEDITOR.htmlParser.fragment + * @param {String} source The HTML to be parsed, filling the fragment. + * @returns {CKEDITOR.htmlParser.fragment} The fragment created. + */ + CKEDITOR.htmlParser.fragment.fromBBCode = function( source ) { + var parser = new CKEDITOR.BBCodeParser(), + fragment = new CKEDITOR.htmlParser.fragment(), + pendingInline = [], + pendingBrs = 0, + currentNode = fragment, + returnPoint; + + function checkPending( newTagName ) { + if ( pendingInline.length > 0 ) { + for ( var i = 0; i < pendingInline.length; i++ ) { + var pendingElement = pendingInline[ i ], + pendingName = pendingElement.name, + pendingDtd = CKEDITOR.dtd[ pendingName ], + currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; + + if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) { + // Get a clone for the pending element. + pendingElement = pendingElement.clone(); + + // Add it to the current node and make it the current, + // so the new element will be added inside of it. + pendingElement.parent = currentNode; + currentNode = pendingElement; + + // Remove the pending element (back the index by one + // to properly process the next entry). + pendingInline.splice( i, 1 ); + i--; + } + } + } + } + + function checkPendingBrs( tagName, closing ) { + var len = currentNode.children.length, + previous = len > 0 && currentNode.children[ len - 1 ], + lineBreakParent = !previous && writer.getRule( tagnameMap[ currentNode.name ], 'breakAfterOpen' ), + lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && writer.getRule( tagnameMap[ previous.name ], 'breakAfterClose' ), + lineBreakCurrent = tagName && writer.getRule( tagnameMap[ tagName ], closing ? 'breakBeforeClose' : 'breakBeforeOpen' ); + + if ( pendingBrs && ( lineBreakParent || lineBreakPrevious || lineBreakCurrent ) ) + pendingBrs--; + + // 1. Either we're at the end of block, where it requires us to compensate the br filler + // removing logic (from htmldataprocessor). + // 2. Or we're at the end of pseudo block, where it requires us to compensate + // the bogus br effect. + if ( pendingBrs && tagName in blockLikeTags ) + pendingBrs++; + + while ( pendingBrs && pendingBrs-- ) + currentNode.children.push( previous = new CKEDITOR.htmlParser.element( 'br' ) ); + } + + function addElement( node, target ) { + checkPendingBrs( node.name, 1 ); + + target = target || currentNode || fragment; + + var len = target.children.length, + previous = len > 0 && target.children[ len - 1 ] || null; + + node.previous = previous; + node.parent = target; + + target.children.push( node ); + + if ( node.returnPoint ) { + currentNode = node.returnPoint; + delete node.returnPoint; + } + } + + parser.onTagOpen = function( tagName, attributes ) { + var element = new CKEDITOR.htmlParser.element( tagName, attributes ); + + // This is a tag to be removed if empty, so do not add it immediately. + if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) { + pendingInline.push( element ); + return; + } + + var currentName = currentNode.name; + + var currentDtd = currentName && ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ); + + // If the element cannot be child of the current element. + if ( currentDtd && !currentDtd[ tagName ] ) { + var reApply = false, + addPoint; // New position to start adding nodes. + + // If the element name is the same as the current element name, + // then just close the current one and append the new one to the + // parent. This situation usually happens with

,

  • ,
    and + //
    , specially in IE. Do not enter in this if block in this case. + if ( tagName == currentName ) + addElement( currentNode, currentNode.parent ); + else if ( tagName in CKEDITOR.dtd.$listItem ) { + parser.onTagOpen( 'ul', {} ); + addPoint = currentNode; + reApply = true; + } else { + addElement( currentNode, currentNode.parent ); + + // The current element is an inline element, which + // cannot hold the new one. Put it in the pending list, + // and try adding the new one after it. + pendingInline.unshift( currentNode ); + reApply = true; + } + + if ( addPoint ) + currentNode = addPoint; + // Try adding it to the return point, or the parent element. + else + currentNode = currentNode.returnPoint || currentNode.parent; + + if ( reApply ) { + parser.onTagOpen.apply( this, arguments ); + return; + } + } + + checkPending( tagName ); + checkPendingBrs( tagName ); + + element.parent = currentNode; + element.returnPoint = returnPoint; + returnPoint = 0; + + if ( element.isEmpty ) + addElement( element ); + else + currentNode = element; + }; + + parser.onTagClose = function( tagName ) { + // Check if there is any pending tag to be closed. + for ( var i = pendingInline.length - 1; i >= 0; i-- ) { + // If found, just remove it from the list. + if ( tagName == pendingInline[ i ].name ) { + pendingInline.splice( i, 1 ); + return; + } + } + + var pendingAdd = [], + newPendingInline = [], + candidate = currentNode; + + while ( candidate.type && candidate.name != tagName ) { + // If this is an inline element, add it to the pending list, if we're + // really closing one of the parents element later, they will continue + // after it. + if ( !candidate._.isBlockLike ) + newPendingInline.unshift( candidate ); + + // This node should be added to it's parent at this point. But, + // it should happen only if the closing tag is really closing + // one of the nodes. So, for now, we just cache it. + pendingAdd.push( candidate ); + + candidate = candidate.parent; + } + + if ( candidate.type ) { + // Add all elements that have been found in the above loop. + for ( i = 0; i < pendingAdd.length; i++ ) { + var node = pendingAdd[ i ]; + addElement( node, node.parent ); + } + + currentNode = candidate; + + + addElement( candidate, candidate.parent ); + + // The parent should start receiving new nodes now, except if + // addElement changed the currentNode. + if ( candidate == currentNode ) + currentNode = currentNode.parent; + + pendingInline = pendingInline.concat( newPendingInline ); + } + }; + + parser.onText = function( text ) { + var currentDtd = CKEDITOR.dtd[ currentNode.name ]; + if ( !currentDtd || currentDtd[ '#' ] ) { + checkPendingBrs(); + checkPending(); + + text.replace( /(\r\n|[\r\n])|[^\r\n]*/g, function( piece, lineBreak ) { + if ( lineBreak !== undefined && lineBreak.length ) + pendingBrs++; + else if ( piece.length ) { + var lastIndex = 0; + + // Create smiley from text emotion. + piece.replace( smileyRegExp, function( match, index ) { + addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, index ) ), currentNode ); + addElement( new CKEDITOR.htmlParser.element( 'smiley', { desc: smileyReverseMap[ match ] } ), currentNode ); + lastIndex = index + match.length; + } ); + + if ( lastIndex != piece.length ) + addElement( new CKEDITOR.htmlParser.text( piece.substring( lastIndex, piece.length ) ), currentNode ); + } + } ); + } + }; + + // Parse it. + parser.parse( CKEDITOR.tools.htmlEncode( source ) ); + + // Close all hanging nodes. + while ( currentNode.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) { + var parent = currentNode.parent, + node = currentNode; + + addElement( node, parent ); + currentNode = parent; + } + + return fragment; + }; + + var BBCodeWriter = CKEDITOR.tools.createClass( { + $: function() { + this._ = { + output: [], + rules: [] + }; + + // List and list item. + this.setRules( 'list', { breakBeforeOpen: 1, breakAfterOpen: 1, breakBeforeClose: 1, breakAfterClose: 1 } ); + + this.setRules( '*', { + breakBeforeOpen: 1, + breakAfterOpen: 0, + breakBeforeClose: 1, + breakAfterClose: 0 + } ); + + this.setRules( 'quote', { + breakBeforeOpen: 1, + breakAfterOpen: 0, + breakBeforeClose: 0, + breakAfterClose: 1 + } ); + }, + + proto: { + // + // Sets formatting rules for a given tag. The possible rules are: + //
      + //
    • breakBeforeOpen: break line before the opener tag for this element.
    • + //
    • breakAfterOpen: break line after the opener tag for this element.
    • + //
    • breakBeforeClose: break line before the closer tag for this element.
    • + //
    • breakAfterClose: break line after the closer tag for this element.
    • + //
    + // + // All rules default to "false". Each call to the function overrides + // already present rules, leaving the undefined untouched. + // + // @param {String} tagName The tag name to which set the rules. + // @param {Object} rules An object containing the element rules. + // @example + // // Break line before and after "img" tags. + // writer.setRules( 'list', + // { + // breakBeforeOpen : true + // breakAfterOpen : true + // }); + setRules: function( tagName, rules ) { + var currentRules = this._.rules[ tagName ]; + + if ( currentRules ) + CKEDITOR.tools.extend( currentRules, rules, true ); + else + this._.rules[ tagName ] = rules; + }, + + getRule: function( tagName, ruleName ) { + return this._.rules[ tagName ] && this._.rules[ tagName ][ ruleName ]; + }, + + openTag: function( tag ) { + if ( tag in bbcodeMap ) { + if ( this.getRule( tag, 'breakBeforeOpen' ) ) + this.lineBreak( 1 ); + + this.write( '[', tag ); + } + }, + + openTagClose: function( tag ) { + if ( tag == 'br' ) + this._.output.push( '\n' ); + else if ( tag in bbcodeMap ) { + this.write( ']' ); + if ( this.getRule( tag, 'breakAfterOpen' ) ) + this.lineBreak( 1 ); + } + }, + + attribute: function( name, val ) { + if ( name == 'option' ) { + this.write( '=', val ); + } + }, + + closeTag: function( tag ) { + if ( tag in bbcodeMap ) { + if ( this.getRule( tag, 'breakBeforeClose' ) ) + this.lineBreak( 1 ); + + tag != '*' && this.write( '[/', tag, ']' ); + + if ( this.getRule( tag, 'breakAfterClose' ) ) + this.lineBreak( 1 ); + } + }, + + text: function( text ) { + this.write( text ); + }, + + comment: function() {}, + + // Output line-break for formatting. + lineBreak: function() { + // Avoid line break when: + // 1) Previous tag already put one. + // 2) We're at output start. + if ( !this._.hasLineBreak && this._.output.length ) { + this.write( '\n' ); + this._.hasLineBreak = 1; + } + }, + + write: function() { + this._.hasLineBreak = 0; + var data = Array.prototype.join.call( arguments, '' ); + this._.output.push( data ); + }, + + reset: function() { + this._.output = []; + this._.hasLineBreak = 0; + }, + + getHtml: function( reset ) { + var bbcode = this._.output.join( '' ); + + if ( reset ) + this.reset(); + + return decodeHtml( bbcode ); + } + } + } ); + + var writer = new BBCodeWriter(); + + CKEDITOR.plugins.add( 'bbcode', { + requires: 'entities', + + // Adapt some critical editor configuration for better support + // of BBCode environment. + beforeInit: function( editor ) { + var config = editor.config; + + CKEDITOR.tools.extend( config, { + // This one is for backwards compatibility only as + // editor#enterMode is already set at this stage (https://dev.ckeditor.com/ticket/11202). + enterMode: CKEDITOR.ENTER_BR, + basicEntities: false, + entities: false, + fillEmptyBlocks: false + }, true ); + + editor.filter.disable(); + + // Since CKEditor 4.3, editor#(active)enterMode is set before + // beforeInit. Properties got to be updated (https://dev.ckeditor.com/ticket/11202). + editor.activeEnterMode = editor.enterMode = CKEDITOR.ENTER_BR; + }, + + init: function( editor ) { + var config = editor.config; + + function BBCodeToHtml( code ) { + var fragment = CKEDITOR.htmlParser.fragment.fromBBCode( code ), + writer = new CKEDITOR.htmlParser.basicWriter(); + + fragment.writeHtml( writer, bbcodeFilter ); + return writer.getHtml( true ); + } + + var bbcodeFilter = new CKEDITOR.htmlParser.filter(); + bbcodeFilter.addRules( { + elements: { + blockquote: function( element ) { + var quoted = new CKEDITOR.htmlParser.element( 'div' ); + quoted.children = element.children; + element.children = [ quoted ]; + var citeText = element.attributes.cite; + if ( citeText ) { + var cite = new CKEDITOR.htmlParser.element( 'cite' ); + cite.add( new CKEDITOR.htmlParser.text( citeText.replace( /^"|"$/g, '' ) ) ); + delete element.attributes.cite; + element.children.unshift( cite ); + } + }, + span: function( element ) { + var bbcode; + if ( ( bbcode = element.attributes.bbcode ) ) { + if ( bbcode == 'img' ) { + element.name = 'img'; + element.attributes.src = element.children[ 0 ].value; + element.children = []; + } else if ( bbcode == 'email' ) { + element.name = 'a'; + element.attributes.href = 'mailto:' + element.children[ 0 ].value; + } + + delete element.attributes.bbcode; + } + }, + ol: function( element ) { + if ( element.attributes.listType ) { + if ( element.attributes.listType != 'decimal' ) + element.attributes.style = 'list-style-type:' + element.attributes.listType; + } else { + element.name = 'ul'; + } + + delete element.attributes.listType; + }, + a: function( element ) { + if ( !element.attributes.href ) + element.attributes.href = element.children[ 0 ].value; + }, + smiley: function( element ) { + element.name = 'img'; + + var description = element.attributes.desc, + image = config.smiley_images[ CKEDITOR.tools.indexOf( config.smiley_descriptions, description ) ], + src = CKEDITOR.tools.htmlEncode( config.smiley_path + image ); + + element.attributes = { + src: src, + 'data-cke-saved-src': src, + title: description, + alt: description + }; + } + } + } ); + + editor.dataProcessor.htmlFilter.addRules( { + elements: { + $: function( element ) { + var attributes = element.attributes, + style = CKEDITOR.tools.parseCssText( attributes.style, 1 ), + value; + + var tagName = element.name; + if ( tagName in convertMap ) + tagName = convertMap[ tagName ]; + else if ( tagName == 'span' ) { + if ( ( value = style.color ) ) { + tagName = 'color'; + value = CKEDITOR.tools.convertRgbToHex( value ); + } else if ( ( value = style[ 'font-size' ] ) ) { + var percentValue = value.match( /(\d+)%$/ ); + if ( percentValue ) { + value = percentValue[ 1 ]; + tagName = 'size'; + } + } + } else if ( tagName == 'ol' || tagName == 'ul' ) { + if ( ( value = style[ 'list-style-type' ] ) ) { + switch ( value ) { + case 'lower-alpha': + value = 'a'; + break; + case 'upper-alpha': + value = 'A'; + break; + } + } else if ( tagName == 'ol' ) { + value = 1; + } + + tagName = 'list'; + } else if ( tagName == 'blockquote' ) { + try { + var cite = element.children[ 0 ], + quoted = element.children[ 1 ], + citeText = cite.name == 'cite' && cite.children[ 0 ].value; + + if ( citeText ) { + value = '"' + citeText + '"'; + element.children = quoted.children; + } + + } catch ( er ) {} + + tagName = 'quote'; + } else if ( tagName == 'a' ) { + if ( ( value = attributes.href ) ) { + if ( value.indexOf( 'mailto:' ) !== -1 ) { + tagName = 'email'; + // [email] should have a single text child with email address. + element.children = [ new CKEDITOR.htmlParser.text( value.replace( 'mailto:', '' ) ) ]; + value = ''; + } else { + var singleton = element.children.length == 1 && element.children[ 0 ]; + if ( singleton && singleton.type == CKEDITOR.NODE_TEXT && singleton.value == value ) + value = ''; + + tagName = 'url'; + } + } + } else if ( tagName == 'img' ) { + element.isEmpty = 0; + + // Translate smiley (image) to text emotion. + var src = attributes[ 'data-cke-saved-src' ] || attributes.src, + alt = attributes.alt; + + if ( src && src.indexOf( editor.config.smiley_path ) != -1 && alt ) + return new CKEDITOR.htmlParser.text( smileyMap[ alt ] ); + else + element.children = [ new CKEDITOR.htmlParser.text( src ) ]; + } + + element.name = tagName; + value && ( element.attributes.option = value ); + + return null; + }, + + div: function( element ) { + var alignment = CKEDITOR.tools.parseCssText( element.attributes.style, 1 )[ 'text-align' ] || ''; + + if ( alignment ) { + element.name = alignment; + return null; + } + }, + + // Remove any bogus br from the end of a pseudo block, + // e.g.
    some text

    paragraph

    + br: function( element ) { + var next = element.next; + if ( next && next.name in blockLikeTags ) + return false; + } + } + }, 1 ); + + editor.dataProcessor.writer = writer; + + function onSetData( evt ) { + var bbcode = evt.data.dataValue; + evt.data.dataValue = BBCodeToHtml( bbcode ); + } + + // Skip the first "setData" call from inline creator, to allow content of + // HTML to be loaded from the page element. + if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) + editor.once( 'contentDom', function() { + editor.on( 'setData', onSetData ); + } ); + else + editor.on( 'setData', onSetData ); + + }, + + afterInit: function( editor ) { + var filters; + if ( editor._.elementsPath ) { + // Eliminate irrelevant elements from displaying, e.g body and p. + if ( ( filters = editor._.elementsPath.filters ) ) { + filters.push( function( element ) { + var htmlName = element.getName(), + name = tagnameMap[ htmlName ] || false; + + // Specialized anchor presents as email. + if ( name == 'link' && element.getAttribute( 'href' ).indexOf( 'mailto:' ) === 0 ) + name = 'email'; + // Styled span could be either size or color. + else if ( htmlName == 'span' ) { + if ( element.getStyle( 'font-size' ) ) + name = 'size'; + else if ( element.getStyle( 'color' ) ) + name = 'color'; + // Styled div could be align + } else if ( htmlName == 'div' && element.getStyle( 'text-align' ) ) { + name = element.getStyle( 'text-align' ); + } else if ( name == 'img' ) { + var src = element.data( 'cke-saved-src' ) || element.getAttribute( 'src' ); + if ( src && src.indexOf( editor.config.smiley_path ) === 0 ) + name = 'smiley'; + } + + return name; + } ); + } + } + } + } ); + +} )(); diff --git a/public/ckeditor/plugins/bbcode/samples/bbcode.html b/public/ckeditor/plugins/bbcode/samples/bbcode.html new file mode 100644 index 00000000..be02c212 --- /dev/null +++ b/public/ckeditor/plugins/bbcode/samples/bbcode.html @@ -0,0 +1,114 @@ + + + + + + BBCode Plugin — CKEditor Sample + + + + + + + + + +

    + CKEditor Samples » BBCode Plugin +

    +
    + This sample is not maintained anymore. Check out its brand new version in CKEditor Examples. +
    +
    +

    + This sample shows how to configure CKEditor to output BBCode format instead of HTML. + Please note that the editor configuration was modified to reflect what is needed in a BBCode editing environment. + Smiley images, for example, were stripped to the emoticons that are commonly used in some BBCode dialects. +

    +

    + Please note that currently there is no standard for the BBCode markup language, so its implementation + for different platforms (message boards, blogs etc.) can vary. This means that before using CKEditor to + output BBCode you may need to adjust the implementation to your own environment. +

    +

    + A snippet of the configuration code can be seen below; check the source of this page for + a full definition: +

    +
    +CKEDITOR.replace( 'editor1', {
    +	extraPlugins: 'bbcode',
    +	toolbar: [
    +		[ 'Source', '-', 'Save', 'NewPage', '-', 'Undo', 'Redo' ],
    +		[ 'Find', 'Replace', '-', 'SelectAll', 'RemoveFormat' ],
    +		[ 'Link', 'Unlink', 'Image' ],
    +		'/',
    +		[ 'FontSize', 'Bold', 'Italic', 'Underline' ],
    +		[ 'NumberedList', 'BulletedList', '-', 'Blockquote' ],
    +		[ 'TextColor', '-', 'Smiley', 'SpecialChar', '-', 'Maximize' ]
    +	],
    +	... some other configurations omitted here
    +});	
    +
    +
    +

    + + + +

    +

    + +

    +
    + + + diff --git a/src/Form/PartType.php b/src/Form/PartType.php index e104814b..ff1ed8a7 100644 --- a/src/Form/PartType.php +++ b/src/Form/PartType.php @@ -33,6 +33,7 @@ namespace App\Form; use App\Entity\Part; +use FOS\CKEditorBundle\Form\Type\CKEditorType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\UrlType; @@ -58,7 +59,7 @@ class PartType extends AbstractType ['attr' => ['min'=>0, 'placeholder' => 'part.mininstock.placeholder'], 'label'=> 'mininstock.label']) ->add('manufacturer_product_url', UrlType::class, ['required'=>false, 'empty_data' => '', 'label'=> 'manufacturer_url.label']) - ->add('comment', TextareaType::class, ['required'=>false, + ->add('comment', CKEditorType::class, ['required'=>false, 'label'=> 'comment.label', 'attr' => ['rows'=> 4], 'help' => 'bbcode.hint']) //Buttons diff --git a/symfony.lock b/symfony.lock index 6c38d290..aa7b5bfe 100644 --- a/symfony.lock +++ b/symfony.lock @@ -99,6 +99,18 @@ "fig/link-util": { "version": "1.0.0" }, + "friendsofsymfony/ckeditor-bundle": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "2.0", + "ref": "8eb1cd0962ded6a6d6e1e5a9b6d3e888f9f94ff6" + }, + "files": [ + "./config/packages/fos_ckeditor.yaml" + ] + }, "jdorn/sql-formatter": { "version": "v1.2.17" }, diff --git a/yarn.lock b/yarn.lock index f09bffc6..f395b610 100644 --- a/yarn.lock +++ b/yarn.lock @@ -599,6 +599,11 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@ckeditor/ckeditor5-build-classic@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-build-classic/-/ckeditor5-build-classic-12.0.0.tgz#0a738809ee5d45be7bf18a474b73cb15902ba871" + integrity sha512-BdD7+u3obrWuPC2kxG/JBXBrtohfPbCzLrNOYE0ITe7qokqmzvfY4WAMwPuI2wBwbuHzuPeGJCnWvjQZgtRZoQ== + "@fortawesome/fontawesome-free@^5.7.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.7.2.tgz#1498c3eb78ee7c78c5488418707de90aaf58d5d7"