├── images ├── screenshot.png └── shortcodable.png ├── css └── shortcodable.css ├── src ├── Extensions │ ├── ShortcodableHtmlEditorField.php │ ├── ShortcodableShortcodeParserExtension.php │ └── ShortcodableParser.php ├── Shortcodable.php └── Controller │ └── ShortcodableController.php ├── .editorconfig ├── _config └── config.yml ├── license.md ├── composer.json ├── _config.php ├── README.md └── javascript ├── editor_plugin.js └── shortcodable.js /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheadawson/silverstripe-shortcodable/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /images/shortcodable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheadawson/silverstripe-shortcodable/HEAD/images/shortcodable.png -------------------------------------------------------------------------------- /css/shortcodable.css: -------------------------------------------------------------------------------- 1 | .htmleditorfield-shortcodable .ss-shortcodable { 2 | padding: 8px 16px; 3 | } 4 | 5 | .htmleditorfield-shortcodable.loading .ss-shortcodable, 6 | .htmleditorfield-shortcodable.loading .Actions{ 7 | opacity: 0; 8 | } 9 | 10 | 11 | .htmleditorfield-shortcodable .ss-shortcodable .field .chzn-container{ 12 | max-width: 100%; 13 | } 14 | 15 | .defaultSkin span.mce_shortcode{ 16 | background: url(../images/shortcodable.png) no-repeat center center; 17 | } -------------------------------------------------------------------------------- /src/Extensions/ShortcodableHtmlEditorField.php: -------------------------------------------------------------------------------- 1 | owner->setAttribute( 14 | 'data-placeholderclasses', 15 | implode(',', Shortcodable::get_shortcodable_classes_with_placeholders()) 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: shortcodable 3 | After: 4 | - framework/* 5 | # - cms/* 6 | --- 7 | 8 | LeftAndMain: 9 | extra_requirements_javascript: 10 | - 'resources/shortcodable/javascript/editor_plugin.js' 11 | - 'resources/shortcodable/javascript/shortcodable.js' 12 | extra_requirements_css: 13 | - 'shortcodable/css/shortcodable.css' 14 | 15 | SilverStripe\Control\Director: 16 | rules: 17 | 'admin/shortcodable': Silverstripe\Shortcodable\Controller\ShortcodableController 18 | 19 | Shortcodable: 20 | htmleditor_names: 21 | - cms 22 | 23 | HtmlEditorField: 24 | extensions: 25 | - ShortcodableHtmlEditorField 26 | 27 | ShortcodeParser: 28 | extensions: 29 | - ShortcodableShortcodeParserExtension 30 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Shea Dawson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sheadawson/silverstripe-shortcodable", 3 | "type": "silverstripe-vendormodule", 4 | "description": "Provides a GUI for CMS users to insert Shortcodes into the HTMLEditorField + an API for developers to define Shortcodable DataObjects and Views", 5 | "keywords": [ 6 | "silverstripe", 7 | "shortcode" 8 | ], 9 | "license": "BSD License", 10 | "authors": [ 11 | { 12 | "name": "Shea Dawson", 13 | "email": "shea@livesource.co.nz" 14 | } 15 | ], 16 | "require": { 17 | "composer/installers": "*", 18 | "silverstripe/framework": "^4" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "SilverStripe\\Shortcodable\\": "src/" 23 | } 24 | }, 25 | "support": { 26 | "issues": "https://github.com/sheadawson/silverstripe-shortcodable/issues" 27 | }, 28 | "extra": { 29 | "installer-name": "shortcodable", 30 | "expose": [ 31 | "css", 32 | "images", 33 | "javascript", 34 | ] 35 | }, 36 | "homepage": "https://github.com/sheadawson/silverstripe-shortcodable" 37 | } 38 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | get(Shortcodable::class, 'htmleditor_names'); 16 | if (is_array($htmlEditorNames)) { 17 | foreach ($htmlEditorNames as $htmlEditorName) { 18 | // HtmlEditorConfig::get($htmlEditorName)->enablePlugins(array( 19 | // 'shortcodable' => sprintf('/resources/%s/javascript/editor_plugin.js', SHORTCODABLE_DIR) 20 | // )); 21 | HtmlEditorConfig::get($htmlEditorName)->addButtonsToLine(1, 'shortcodable'); 22 | } 23 | } 24 | 25 | // register classes added via yml config 26 | $classes = Config::inst()->get(Shortcodable::class, 'shortcodable_classes'); 27 | Shortcodable::register_classes($classes); 28 | -------------------------------------------------------------------------------- /src/Extensions/ShortcodableShortcodeParserExtension.php: -------------------------------------------------------------------------------- 1 | owner; 13 | // Check the shortcode type and convert wrapper to div if block type 14 | // Regex examples: https://regex101.com/r/bFtD9o/3 15 | $content = preg_replace_callback( 16 | '|]*?)?>\s*?\[((.*)([\s,].*)?)\]\s*?

|U', 17 | function ($matches) use($parser) { 18 | $shortcodeName = $matches[3]; 19 | // Since we're only concerned with shortcodable objects we know the 20 | // shortcode name will be the class name so don't have to look it up 21 | if ($shortcodeName && $parser->registered($shortcodeName)) { 22 | if (Config::inst()->get($shortcodeName, 'shortcodable_is_block') && Config::inst()->get($shortcodeName, 'disable_wrapper')) { 23 | return "[$matches[2]]"; 24 | } 25 | if (Config::inst()->get($shortcodeName, 'shortcodable_is_block')) { 26 | return "[$matches[2]]"; 27 | } 28 | } 29 | return $matches[0]; 30 | }, 31 | $content 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Shortcodable.php: -------------------------------------------------------------------------------- 1 | hasMethod('parse_shortcode')) { 33 | user_error("Failed to register \"$class\" with shortcodable. $class must have the method parse_shortcode(). See /shortcodable/README.md", E_USER_ERROR); 34 | } 35 | ShortcodeParser::get('default')->register($class, array($class, 'parse_shortcode')); 36 | singleton(ShortcodableParser::class)->register($class); 37 | } 38 | } 39 | 40 | public static function get_shortcodable_classes() 41 | { 42 | return Config::inst()->get(Shortcodable::class, 'shortcodable_classes'); 43 | } 44 | 45 | public static function get_shortcodable_classes_fordropdown() 46 | { 47 | $classList = self::get_shortcodable_classes(); 48 | $classes = array(); 49 | if (is_array($classList)) { 50 | foreach ($classList as $class) { 51 | if (singleton($class)->hasMethod('singular_name')) { 52 | $classes[$class] = singleton($class)->singular_name(); 53 | } else { 54 | $classes[$class] = $class; 55 | } 56 | } 57 | } 58 | return $classes; 59 | } 60 | 61 | public static function get_shortcodable_classes_with_placeholders() 62 | { 63 | $classes = array(); 64 | foreach (self::get_shortcodable_classes() as $class) { 65 | if (singleton($class)->hasMethod('getShortcodePlaceHolder')) { 66 | $classes[] = $class; 67 | } 68 | } 69 | return $classes; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Shortcodable 4 2 | 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sheadawson/silverstripe-shortcodable/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sheadawson/silverstripe-shortcodable/?branch=master) 4 | 5 | ![Screenshot](https://raw.github.com/sheadawson/silverstripe-shortcodable/master/images/screenshot.png) 6 | 7 | ## SS4 compatible version is WIP. Please submit PRs if you'd like to help move this along! 8 | 9 | What’s working: 10 | 11 | * The TinyMCE button/popup 12 | * The form in the popup dialog 13 | 14 | What’s not working: 15 | 16 | * Placeholders 17 | * Editing an existing shortcode 18 | * Probably other things 19 | 20 | Provides a GUI for CMS users to insert Shortcodes into the HTMLEditorField + an API for developers to define Shortcodable DataObjects and Views. This allows CMS users to easily embed and customise DataObjects and templated HTML snippets anywhere amongst their page content. Shortcodes can optionally be represented in the WYSIWYG with a custom placeholder image. 21 | 22 | ## Requirements 23 | * SilverStripe 4 + 24 | 25 | See 3.x branch/releases for SilverStripe SS 3.5 compatibility 26 | See 2.x branch/releases for SilverStripe SS 3.1 - 3.4 compatibility 27 | 28 | ## Installation 29 | Install via composer, run dev/build 30 | ``` 31 | composer require sheadawson/silverstripe-shortcodable 32 | ``` 33 | 34 | ## Configuration 35 | See [this gist](https://gist.github.com/sheadawson/12c5e5a2b42272bd90f703941450d677) for a well documented example of a Shortcodable ImageGallery to get you started. This example is for a subclass of DataObject. If your shortcodable object doesn't need it's own database record, you can use the same example but use ViewableData as the parent class. 36 | 37 | #### TinyMCE block elements 38 | In SilverStripe 3 shortcodes tend to get wrapped in paragraph elements, which is a problem if your shortcode will be rendered as a block element. To get around this you can flag shortcodable classes as block elements with a config setting. If you don't want to replace the paragraph tag with a div this can be disabled as well. 39 | 40 | ```yml 41 | MyShortcodableClass: 42 | shortcodable_is_block: true 43 | disable_wrapper: true 44 | ``` 45 | 46 | ## CMS Usage 47 | Once installed a new icon will appear in the CMS HTMLEditor toolbar. It looks like this: 48 | ![icon](https://raw.github.com/sheadawson/silverstripe-shortcodable/master/images/shortcodable.png) 49 | 50 | Clicking the toolbar will open a popup that allows you to insert a shortcode into the editor. 51 | 52 | Highlighting an existing shortcode tag in the editor before clicking the shortcode icon will open the popup to allow editing of the selected shortcode tag. 53 | 54 | Double clicking a shortcode placeholder in the editor will also open the popup to allow editing of the shortcode. 55 | 56 | ## Upgrading from 1.x 57 | Shortcodable 2.0 has an improved method for applying Shortcodable to DataObjects. We no longer use an interface, as this didn't allow for Shortcodable to be applied to core classes such as File, Member, Page etc without changing core code. Instead, Shortcodable is applied to your Objects via yml config. Some methods have also changed from statics to normal methods. See updated examples below. 58 | -------------------------------------------------------------------------------- /javascript/editor_plugin.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof tinymce !== 'undefined') { 3 | 4 | tinymce.create('tinymce.plugins.shortcodable', { 5 | getInfo: function () { 6 | return { 7 | longname: 'Shortcodable - Shortcode UI plugin for SilverStripe', 8 | author: 'Shea Dawson', 9 | authorurl: 'http://www.livesource.co.nz/', 10 | infourl: 'http://www.livesource.co.nz/', 11 | version: "1.0" 12 | }; 13 | }, 14 | 15 | init: function (ed, url) { 16 | var me = tinyMCE.activeEditor.plugins.shortcodable; 17 | 18 | ed.addButton('shortcodable', { 19 | title: 'Insert Shortcode', 20 | cmd: 'shortcodable', 21 | 'class': 'mce_shortcode' 22 | }); 23 | 24 | ed.addCommand('shortcodable', function (ed) { 25 | jQuery('#' + this.id).entwine('ss').openShortcodeDialog(); 26 | }); 27 | 28 | // On load replace shorcode with placeholder. 29 | ed.onLoadContent.add(function (ed, o) { 30 | var newContent = me.replaceShortcodesWithPlaceholders(o.content, ed); 31 | ed.execCommand('mceSetContent', false, newContent, false); 32 | }); 33 | 34 | ed.onDblClick.add(function (ed, e) { 35 | var dom = ed.dom, node = e.target; 36 | if (node.nodeName === 'IMG' && dom.hasClass(node, 'shortcode-placeholder') && e.button !== 2) { 37 | ed.execCommand('shortcodable'); 38 | } 39 | }); 40 | }, 41 | 42 | // replace shortcode strings with placeholder images 43 | replaceShortcodesWithPlaceholders: function (content, editor) { 44 | var plugin = tinyMCE.activeEditor.plugins.shortcodable; 45 | var placeholderClasses = jQuery('#' + editor.id).entwine('ss').getPlaceholderClasses(); 46 | 47 | if (placeholderClasses) { 48 | return content.replace(/\[([a-z_]+)\s*([^\]]*)\]/gi, function (found, name, params) { 49 | var id = plugin.getAttribute(params, 'id'); 50 | if (placeholderClasses.indexOf(name) != -1) { 51 | var src = encodeURI('admin/shortcodable/shortcodePlaceHolder/' + name + '/' + id + '?Shortcode=[' + name + ' ' + params + ']'); 52 | var img = jQuery('') 53 | .attr('class', 'shortcode-placeholder mceItem') 54 | .attr('title', name + ' ' + params) 55 | .attr('src', src); 56 | return img.prop('outerHTML'); 57 | } 58 | 59 | return found; 60 | }); 61 | } else { 62 | return content; 63 | } 64 | }, 65 | 66 | // replace placeholder tags with shortcodes 67 | replacePlaceholdersWithShortcodes: function (co) { 68 | var content = jQuery(co); 69 | content.find('.shortcode-placeholder').each(function () { 70 | var el = jQuery(this); 71 | var shortCode = '[' + tinymce.trim(el.attr('title')) + ']'; 72 | el.replaceWith(shortCode); 73 | }); 74 | var originalContent = ''; 75 | content.each(function () { 76 | if (this.outerHTML !== undefined) { 77 | originalContent += this.outerHTML; 78 | } 79 | }); 80 | return originalContent; 81 | }, 82 | 83 | // get an attribute from a shortcode string by it's key 84 | getAttribute: function (string, key) { 85 | var attr = new RegExp(key + '=\"([^\"]+)\"', 'g').exec(string); 86 | return attr ? tinymce.DOM.decode(attr[1]) : ''; 87 | } 88 | }); 89 | 90 | // Adds the plugin class to the list of available TinyMCE plugins 91 | tinymce.PluginManager.add("shortcodable", tinymce.plugins.shortcodable); 92 | } 93 | })(); 94 | -------------------------------------------------------------------------------- /src/Extensions/ShortcodableParser.php: -------------------------------------------------------------------------------- 1 | shortcodes[$name] = $name; 26 | } 27 | 28 | /** 29 | * @param string $text 30 | * @return array 31 | */ 32 | public function get_pattern($text) 33 | { 34 | $pattern = $this->get_shortcode_regex(); 35 | preg_match_all("/$pattern/s", $text, $c); 36 | 37 | return $c; 38 | } 39 | 40 | /** 41 | * @param string $content 42 | * @return array 43 | */ 44 | public function parse_atts($content) 45 | { 46 | $content = preg_match_all('/([^ =]*)=(\'([^\']*)\'|\"([^\"]*)\"|([^ ]*))/', trim($content), $c); 47 | list($dummy, $keys, $values) = array_values($c); 48 | $c = array(); 49 | foreach ($keys as $key => $value) { 50 | $value = trim($values[ $key ], "\"'"); 51 | $type = is_numeric($value) ? 'int' : 'string'; 52 | $type = in_array(strtolower($value), array('true', 'false')) ? 'bool' : $type; 53 | switch ($type) { 54 | case 'int': $value = (int) $value; break; 55 | case 'bool': $value = strtolower($value) == 'true'; break; 56 | } 57 | $c[ $keys[ $key ] ] = $value; 58 | } 59 | 60 | return $c; 61 | } 62 | 63 | /** 64 | * @param array $output 65 | * @param string $text 66 | * @param boolean $child 67 | * @return array 68 | */ 69 | public function the_shortcodes($output, $text, $child = false) 70 | { 71 | $patts = $this->get_pattern($text); 72 | $t = array_filter($this->get_pattern($text)); 73 | if (!empty($t)) { 74 | list($d, $d, $parents, $atts, $d, $contents) = $patts; 75 | $out2 = array(); 76 | $n = 0; 77 | foreach ($parents as $k => $parent) { 78 | ++$n; 79 | $name = $child ? 'child'.$n : $n; 80 | $t = array_filter($this->get_pattern($contents[ $k ])); 81 | $t_s = $this->the_shortcodes($out2, $contents[ $k ], true); 82 | $output[ $name ] = array('name' => $parents[ $k ]); 83 | $output[ $name ]['atts'] = $this->parse_atts($atts[ $k ]); 84 | $output[ $name ]['original_content'] = $contents[ $k ]; 85 | $output[ $name ]['content'] = !empty($t) && !empty($t_s) ? $t_s : $contents[ $k ]; 86 | } 87 | } 88 | 89 | return array_values($output); 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function get_shortcode_regex() 96 | { 97 | $shortcode_tags = $this->shortcodes; 98 | $tagnames = array_keys($shortcode_tags); 99 | $tagregexp = implode('|', array_map('preg_quote', $tagnames)); 100 | 101 | // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag() 102 | // Also, see shortcode_unautop() and shortcode.js. 103 | return 104 | '\\[' // Opening bracket 105 | .'(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]] 106 | ."($tagregexp)" // 2: Shortcode name 107 | .'(?![\\w-])' // Not followed by word character or hyphen 108 | .'(' // 3: Unroll the loop: Inside the opening shortcode tag 109 | .'[^\\]\\/]*' // Not a closing bracket or forward slash 110 | .'(?:' 111 | .'\\/(?!\\])' // A forward slash not followed by a closing bracket 112 | .'[^\\]\\/]*' // Not a closing bracket or forward slash 113 | .')*?' 114 | .')' 115 | .'(?:' 116 | .'(\\/)' // 4: Self closing tag ... 117 | .'\\]' // ... and closing bracket 118 | .'|' 119 | .'\\]' // Closing bracket 120 | .'(?:' 121 | .'(' // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags 122 | .'[^\\[]*+' // Not an opening bracket 123 | .'(?:' 124 | .'\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag 125 | .'[^\\[]*+' // Not an opening bracket 126 | .')*+' 127 | .')' 128 | .'\\[\\/\\2\\]' // Closing shortcode tag 129 | .')?' 130 | .')' 131 | .'(\\]?)'; // 6: Optional second closing brocket for escaping shortcodes: [[tag]] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /javascript/shortcodable.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.entwine('ss', function($) { 3 | 4 | // handle change on shortcode-type field 5 | $('select.shortcode-type').entwine({ 6 | onchange: function(){ 7 | this.parents('form:first').reloadForm('type', this.val()); 8 | } 9 | }); 10 | 11 | // add shortcode controller url to cms-editor-dialogs 12 | $('#cms-editor-dialogs').entwine({ 13 | onmatch: function(){ 14 | this.attr('data-url-shortcodeform', 'admin/shortcodable/ShortcodeForm/forTemplate'); 15 | } 16 | }); 17 | 18 | // open shortcode dialog 19 | $('textarea.htmleditor').entwine({ 20 | openShortcodeDialog: function() { 21 | this.openDialog('shortcode'); 22 | }, 23 | getPlaceholderClasses: function() { 24 | var classes = $(this).data('placeholderclasses'); 25 | if (classes) { 26 | return classes.split(','); 27 | } 28 | }, 29 | /** 30 | * Make sure the editor has flushed all it's buffers before the form is submitted. 31 | */ 32 | 'from .cms-edit-form': { 33 | onbeforesubmitform: function(e) { 34 | // Save the updated content here, rather than _after_ replacing the placeholders 35 | // otherwise you're replacing the shortcode html with the shortcode, then writing 36 | // the html back to the textarea value, overriding what the shortcode conversion 37 | // process has done 38 | this._super(e); 39 | 40 | var shortcodable = tinyMCE.activeEditor.plugins.shortcodable; 41 | if (shortcodable) { 42 | var ed = this.getEditor(); 43 | var newContent = shortcodable.replacePlaceholdersWithShortcodes($(this).val(), ed); 44 | $(this).val(newContent); 45 | } 46 | } 47 | }, 48 | }); 49 | 50 | $('form.htmleditorfield-shortcodable').entwine({ 51 | // load the shortcode form into the dialog 52 | reloadForm: function(from, data) { 53 | var postdata = {}; 54 | if(from == 'type'){ 55 | postdata.ShortcodeType = data; 56 | }else if(from =='shortcode'){ 57 | postdata.Shortcode = data; 58 | } 59 | 60 | this.addClass('loading'); 61 | 62 | var url = $('#cms-editor-dialogs').attr('data-url-shortcodeform'); 63 | 64 | $.post(url, postdata, function(data){ 65 | var form = $('form.htmleditorfield-shortcodable') 66 | form.find('fieldset').replaceWith($(data).find('fieldset')).show(); 67 | form.removeClass('loading'); 68 | }); 69 | return this; 70 | }, 71 | // shortcode form submit handler 72 | onsubmit: function(e) { 73 | this.insertShortcode(); 74 | this.getDialog().close(); 75 | return false; 76 | }, 77 | // insert shortcode into editor 78 | insertShortcode: function() { 79 | var shortcode = this.getHTML(); 80 | if (shortcode.length) { 81 | this.modifySelection(function(ed){ 82 | var shortcodable = tinyMCE.activeEditor.plugins.shortcodable; 83 | ed.replaceContent(shortcode); 84 | var newContent = shortcodable.replaceShortcodesWithPlaceholders(ed.getContent(), ed.getInstance()); 85 | // console.log(newContent); 86 | ed.setContent(newContent); 87 | }); 88 | } 89 | }, 90 | // get the html to insert 91 | getHTML: function(){ 92 | var data = this.getAttributes(); 93 | var html = data.shortcodeType; 94 | 95 | for (var key in data.attributes) { 96 | html += ' ' + key + '="' + data.attributes[key] + '"'; 97 | } 98 | 99 | if (html.length) { 100 | return "[" + html + "]"; 101 | } else { 102 | return ''; 103 | } 104 | }, 105 | // get shortcode attributes from shortcode form 106 | getAttributes: function() { 107 | var attributes = {}; 108 | var shortcodeType = this.find(':input[name=ShortcodeType]').val(); 109 | var id = this.find(':input[name=id]').val(); 110 | if (id) { 111 | attributes['id'] = id; 112 | } 113 | var data = JSON.stringify(this.serializeArray()); 114 | 115 | var attributesComposite = this.find('.attributes-composite'); 116 | if (attributesComposite.length) { 117 | attributesComposite.find(":input").each(function(){ 118 | var attributeField = $(this); 119 | var attributeVal = attributeField.val(); 120 | var attributeName = attributeField.prop('name'); 121 | 122 | if(attributeField.is('.checkbox') && !attributeField.is(':checked')) { 123 | return true; // skip unchecked checkboxes 124 | } 125 | 126 | if(attributeVal !== ''){ 127 | if (attributeName.indexOf('[') > -1) { 128 | var key = attributeName.substring(0, attributeName.indexOf('[')); 129 | if (typeof attributes[key] != 'undefined') { 130 | attributes[key] += ',' + attributeVal; 131 | } else { 132 | attributes[key] = attributeVal; 133 | } 134 | } else { 135 | if(attributeField.is('.checkbox')) { 136 | attributes[attributeField.prop('name')] = attributeField.is(':checked') ? 1 : 0; 137 | } else { 138 | attributes[attributeField.prop('name')] = attributeVal; 139 | } 140 | } 141 | } 142 | }); 143 | } 144 | 145 | return { 146 | 'shortcodeType' : this.find(':input[name=ShortcodeType]').val(), 147 | 'attributes' : attributes 148 | }; 149 | }, 150 | 151 | 152 | resetFields: function() { 153 | this._super(); 154 | // trigger a change on the shortcode type field to reload all fields 155 | this.find(':input[name=ShortcodeType]').val(''); 156 | this.find('.attributes-composite').hide(); 157 | this.find('#id.field').hide(); 158 | }, 159 | /** 160 | * Updates the state of the dialog inputs to match the editor selection. 161 | * If selection does not contain a shortcode, resets the fields. 162 | */ 163 | updateFromEditor: function() { 164 | var shortcode = this.getCurrentShortcode().trim(); 165 | this.reloadForm('shortcode', shortcode) 166 | }, 167 | getCurrentShortcode: function() { 168 | var selection = $(this.getSelection()), selectionText = selection.text(); 169 | if (selection.attr('title') !== undefined) { 170 | return '[' + selection.attr('title') + ']'; 171 | } 172 | return selectionText; 173 | } 174 | }); 175 | }); 176 | })(jQuery); 177 | -------------------------------------------------------------------------------- /src/Controller/ShortcodableController.php: -------------------------------------------------------------------------------- 1 | 'CMS_ACCESS_LeftAndMain', 34 | 'handleEdit' => 'CMS_ACCESS_LeftAndMain', 35 | 'shortcodePlaceHolder' => 'CMS_ACCESS_LeftAndMain' 36 | ); 37 | 38 | /** 39 | * @var array 40 | */ 41 | private static $url_handlers = array( 42 | 'edit/$ShortcodeType!/$Action//$ID/$OtherID' => 'handleEdit' 43 | ); 44 | 45 | /** 46 | * @var string 47 | */ 48 | protected $shortcodableclass; 49 | 50 | /** 51 | * @var boolean 52 | */ 53 | protected $isnew = true; 54 | 55 | /** 56 | * @var array 57 | */ 58 | protected $shortcodedata; 59 | 60 | /** 61 | * Get the shortcodable class by whatever means possible. 62 | * Determine if this is a new shortcode, or editing an existing one. 63 | */ 64 | public function init() 65 | { 66 | parent::init(); 67 | if ($data = $this->getShortcodeData()) { 68 | $this->isnew = false; 69 | $this->shortcodableclass = $data['name']; 70 | } elseif ($type = $this->request->requestVar('ShortcodeType')) { 71 | $this->shortcodableclass = $type; 72 | } else { 73 | $this->shortcodableclass = $this->request->param('ShortcodeType'); 74 | } 75 | } 76 | 77 | /** 78 | * Point to edit link, if shortcodable class exists. 79 | */ 80 | public function Link($action = null) 81 | { 82 | if ($this->shortcodableclass) { 83 | return Controller::join_links( 84 | $this->config()->url_base, 85 | $this->config()->sc_url_segment, 86 | 'edit', 87 | $this->shortcodableclass 88 | ); 89 | } 90 | return Controller::join_links($this->config()->url_base, $this->config()->sc_url_segment, $action); 91 | } 92 | 93 | /** 94 | * handleEdit 95 | */ 96 | public function handleEdit(HTTPRequest $request) 97 | { 98 | $this->shortcodableclass = $request->param('ShortcodeType'); 99 | return $this->handleAction($request, $action = $request->param('Action')); 100 | } 101 | 102 | /** 103 | * Get the shortcode data from the request. 104 | * @return array shortcodedata 105 | */ 106 | protected function getShortcodeData() 107 | { 108 | if($this->shortcodedata){ 109 | return $this->shortcodedata; 110 | } 111 | $data = false; 112 | if($shortcode = $this->request->requestVar('Shortcode')){ 113 | //remove BOM inside string on cursor position... 114 | $shortcode = str_replace("\xEF\xBB\xBF", '', $shortcode); 115 | $data = singleton('\Silverstripe\Shortcodable\ShortcodableParser')->the_shortcodes(array(), $shortcode); 116 | if(isset($data[0])){ 117 | $this->shortcodedata = $data[0]; 118 | return $this->shortcodedata; 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Provides a GUI for the insert/edit shortcode popup. 125 | * 126 | * @return Form 127 | **/ 128 | public function ShortcodeForm() 129 | { 130 | Config::inst()->update('SSViewer', 'theme_enabled', false); 131 | $classes = Shortcodable::get_shortcodable_classes_fordropdown(); 132 | $classname = $this->shortcodableclass; 133 | 134 | if ($this->isnew) { 135 | $headingText = _t('Shortcodable.EDITSHORTCODE', 'Edit Shortcode'); 136 | } else { 137 | $headingText = sprintf( 138 | _t('Shortcodable.EDITSHORTCODE', 'Edit %s Shortcode'), 139 | singleton($this->shortcodableclass)->singular_name() 140 | ); 141 | } 142 | 143 | // essential fields 144 | $fields = FieldList::create(array( 145 | CompositeField::create( 146 | LiteralField::create( 147 | 'Heading', 148 | sprintf('

%s

', $headingText) 149 | ) 150 | )->addExtraClass('CompositeField composite cms-content-header nolabel'), 151 | LiteralField::create('shortcodablefields', '
'), 152 | DropdownField::create('ShortcodeType', _t('Shortcodable.SHORTCODETYPE', 'Shortcode type'), $classes, $classname) 153 | ->setHasEmptyDefault(true) 154 | ->addExtraClass('shortcode-type') 155 | )); 156 | 157 | // attribute and object id fields 158 | if ($classname && class_exists($classname)) { 159 | $class = singleton($classname); 160 | if (is_subclass_of($class, 'DataObject')) { 161 | if (singleton($classname)->hasMethod('getShortcodableRecords')) { 162 | $dataObjectSource = singleton($classname)->getShortcodableRecords(); 163 | } else { 164 | $dataObjectSource = $classname::get()->map()->toArray(); 165 | } 166 | $fields->push( 167 | DropdownField::create('id', $class->singular_name(), $dataObjectSource) 168 | ->setHasEmptyDefault(true) 169 | ); 170 | } 171 | if (singleton($classname)->hasMethod('getShortcodeFields')) { 172 | if ($attrFields = singleton($classname)->getShortcodeFields()) { 173 | $fields->push( 174 | CompositeField::create($attrFields) 175 | ->addExtraClass('attributes-composite') 176 | ->setName('AttributesCompositeField') 177 | ); 178 | } 179 | } 180 | } 181 | 182 | // actions 183 | $actions = FieldList::create(array( 184 | FormAction::create('insert', _t('Shortcodable.BUTTONINSERTSHORTCODE', 'Insert shortcode')) 185 | ->addExtraClass('ss-ui-action-constructive') 186 | ->setAttribute('data-icon', 'accept') 187 | ->setUseButtonTag(true) 188 | )); 189 | 190 | // form 191 | $form = Form::create($this, 'ShortcodeForm', $fields, $actions) 192 | ->loadDataFrom($this) 193 | ->addExtraClass('htmleditorfield-form htmleditorfield-shortcodable cms-dialog-content'); 194 | 195 | $this->extend('updateShortcodeForm', $form); 196 | 197 | $fields->push(LiteralField::create('shortcodablefieldsend', '
')); 198 | 199 | if ($data = $this->getShortcodeData()) { 200 | $form->loadDataFrom($data['atts']); 201 | 202 | // special treatment for setting value of UploadFields 203 | foreach ($form->Fields()->dataFields() as $field) { 204 | if (is_a($field, 'UploadField') && isset($data['atts'][$field->getName()])) { 205 | $field->setValue(array('Files' => explode(',', $data['atts'][$field->getName()]))); 206 | } 207 | } 208 | } 209 | 210 | return $form; 211 | } 212 | 213 | /** 214 | * Generates shortcode placeholder to display inside TinyMCE instead of the shortcode. 215 | * 216 | * @return void 217 | */ 218 | public function shortcodePlaceHolder($request) 219 | { 220 | if (!Permission::check('CMS_ACCESS_CMSMain')) { 221 | return; 222 | } 223 | 224 | $classname = $request->param('ID'); 225 | $id = $request->param('OtherID'); 226 | 227 | if (!class_exists($classname)) { 228 | return; 229 | } 230 | 231 | if ($id && is_subclass_of($classname, DataObject::class)) { 232 | $object = $classname::get()->byID($id); 233 | } else { 234 | $object = singleton($classname); 235 | } 236 | 237 | if ($object->hasMethod('getShortcodePlaceHolder')) { 238 | $attributes = null; 239 | if ($shortcode = $request->requestVar('Shortcode')) { 240 | $shortcode = str_replace("\xEF\xBB\xBF", '', $shortcode); //remove BOM inside string on cursor position... 241 | $shortcodeData = singleton('\Silverstripe\Shortcodable\ShortcodableParser')->the_shortcodes(array(), $shortcode); 242 | if (isset($shortcodeData[0])) { 243 | $attributes = $shortcodeData[0]['atts']; 244 | } 245 | } 246 | 247 | $link = $object->getShortcodePlaceholder($attributes); 248 | return $this->redirect($link); 249 | } 250 | } 251 | } 252 | --------------------------------------------------------------------------------