It’s been three months since Magento Imagine, and there’s been an eerie silence on the development side of eBay’s Magento platform. After a loud and proud release of Magento Enterprise 1.13, a more quiet release of Magento Community Edition 1.8 Alpha, and a flurry of activity fixing SEO related bugs in EE, there’s been nary a peep out of the Magento core development team.
In an age where everyone’s copying Apple, it’s hard to tell if this is a prelude to a big exciting announcement, or if Magento’s slowly transitioning into maintenance mode, preferring to use business skills to solve their customer’s problems instead of technological skills. Only time will tell on that front.
This leaves modern web developers supporting Magento 1.x systems with a maintenance dilemma. As the web marches on with HTML5, there’s small technological glitches showing around Magento’s edges. In particular, the CMS editor strips new HTML5 attributes from elements added in the source editor.
It would be great if eBay/Magento would address these issues directly and promptly. Fortunately for us, Magento’s an open source system built on other open source technologies, which means we don’t need to wait for a vendor. We can fix the problem ourselves.
In today’s article we’re going cover the reasons for this bug, as well as examine the key technical details of a new module I created to solve this problem.
If you’re the impatient type, you can clone the module on GitHub or download a Magento Connect package and install it yourself.
The Problem
Step one to diagnosing any programming problem is isolating the bug. If you’re working in an engineering aware organization, you’ll often have specific test cases prepared for you by the bug reporter. However, more often than not, you’ll get a vague description of the problem. Either way, you’ll want to recreate the bug yourself before guessing at a cause.
To do so
- Navigate to the
CMS -> Pages
menu in the Magento admin console - Click on the
Add New Page
button -
Enter
Bug Report
for the page title -
Enter
bug-report
for the URL key -
Select all store views in the Store View field
-
Click on the Content Tab
-
Enter
Bug Report
in the Content Heading field -
If necessary, hide the WYSIWYG editor by clicking the
Show/Hide
editor button -
Enter
<input type="text" placeholder="enter value...'/>
in the content text area -
Click the “Save and Continue Button”
Expected Behavior: Content saves and HTML input shows placeholder text
Actual Behavior: Content saves and HTML input does not show placeholder text, and placeholder="..."
is stripped from HTML
This may seem like an excessive level of detail, but by recreating all the steps needed to reproduce a bug, you reduce the chances of a developer facing a “works for me” problem. If a developer can’t reproduce the problem, they can’t fix the problem.
With the bug behavior, reproduction steps, and expected behavior all documented, we’re ready to start in on the problem.
Between the Database and the Browser
If you look at the HTML source of the saved page in the Magento admin, you’ll confirm that the placeholder
attribute is missing
Your first thought may be to start digging through the Magento core code and examining what sort of processing the CMS source through before and after saving. However — remember that you’re not looking at the real output of a page when your view things through a browser. Let’s take a look at the actual page source. Not the currently rendered DOM — but the raw View -> Developer -> View Source
<textarea name="content" title="" id="page_content" class="textarea required-entry" style="height:36em;" rows="2" cols="15" class=" required-entry" >
<input type="text" placeholder="enter value...'/>
</textarea>
Here you see some characters have been converted to HTML entities, but more importantly the placeholder
attribute has actually been rendered!
While we’re still not sure what’s going on, we do know one thing. This is strictly a browser problem, and has nothing to do with Magento’s PHP code.
Tiny Moxiecode Content Editor
Magento’s CMS system, like most modern web CMS systems, has a WYSIWYG editor. Specifically, they use the popular and venerated TinyMCE editor.
TinyMCE came to prominence in a time when HTML5 was just getting off the ground, and its default HTML sanitizing features assuming a circa 2007 list of tags and attributes. This means tag attributes like ‘placeholder’ will be stripped from the document when TinyMCE runs its client side filter.
That’s what’s happening to our attribute above. Fortunately, the TinyMCE developers were smart enough to make the list of valid tags and attributes configurable. All we need to do is figure out how to add the extended_valid_elements
attribute to TinyMCE’s initialization object. Unfortunately, the right way to do this isn’t as straight forward as you might think.
Magento’s TinyMCE Implementation
If you browser to an editor page in the admin and view the HTML page source, you’ll find TinyMCE initialization code that looks something like this
wysiwygpage_content = new tinyMceWysiwygSetup(...);
As they have elsewhere, Magento’s created a PrototypeJS based object wrapper for the native TinyMCE functionality. You can find the tinyMceWysiwygSetup
object’s definition in the following file
//File js/mage/adminhtml/wysiwyg/tiny_mce/setup.js
var tinyMceWysiwygSetup = Class.create();
tinyMceWysiwygSetup.prototype =
{
//...
}
and find the native TinyMCE initialization in this class’s setup
method
//File js/mage/adminhtml/wysiwyg/tiny_mce/setup.js
setup: function(mode)
{
//...
tinyMCE.init(this.getSettings(mode));
},
While straight forward enough, this presents a problem for third party developers like us. The Magento core team has created what is, quite possibly, the most extensible PHP system the world has ever seen, but there’s no template replacement or block rewrite that’s going to help us here. The TinyMCE configuration string is hard coded as a call to the getSettings
method. If you take a look at the getSettings
definition, you’ll see code like this
//File js/mage/adminhtml/wysiwyg/tiny_mce/setup.js
var settings = {
mode : (mode != undefined ? mode : 'none'),
elements : this.id,
theme : 'advanced',
plugins : plugins,
...
Magento is dynamically creating a settings element for the TinyMCE editor, but it’s doing do with a hard coded list of properties. As written, the tinyMceWysiwygSetup
object is incapable of supporting the extended_valid_elements
setting, or any other setting not hard coded in the getSettings
method.
While you might want to get on your soap box and grumble-preach about a poor design, this is an incredibly common pattern in modern software development.
Whether explicitly stated or not, the original goal of the tinyMceWysiwygSetup
object was the limit the TinyMCE editor to a known list of functionality such that it was a more predicable component for the developer. This predictability allowed the developer to meet their deadlines, and prevented the editor from behaving unpredictably due to the work of other programmers or designers.
However, this isn’t an article about developer politics. We need to find a way to patch the Magento editor, and do so without a wholesale replacement of TinyMCE, or a direct hacking of a core javascript file.
Javascript “Class Rewrites”
Thanks to its ubiquitous nature (it’s in every browser), and some key evangelism by Yahoo and the Mozilla Foundation, javascript has exploded as the language to learn in the programming community. Where PHP has a strict class/object system which requires a complicated factory method/configuration to achieve class rewrites, javascript’s object system (particularly as used by PrototypeJS) has built in support for method replacement.
Specifically, if we modify the prototype
property of our class before an object is instantiated, we can actually redefine the getSettings
method
if(window.tinyMceWysiwygSetup)
{
tinyMceWysiwygSetup.prototype.getSettings = function(mode)
{
//...new getSettings here..
}
}
We wrapped our code above in a conditional block. If the global variable tinyMceWysiwygSetup
(our PrototypeJS class) is not defined, we’ll want to skip everything to avoid any javascript errors. Remember, tinyMceWysiwygSetup
is the PrototypeJS class, and wysiwygpage_content
wysiwygpage_content = new tinyMceWysiwygSetup(...);
is the object we instantiate from the class.
While the above code would allow us to completely redefine the getSettings
method, we now have the problem of how to redefine it. We could copy in the entire original source, and add our own extended_valid_elements
property at the end — but if we do that we miss any changes future versions of setup.js
may have (bug fixes, new functionality, etc).
If this were a Magento PHP class rewrite, we’d just call the parent method
$settings = parent::getSettings();
$settings['extended_valid_elements'] = '...';
but that’s not how PrototypeJS and Javascript’s object model work. Fortunately, javascript is flexible enough to provide a workaround.
tinyMceWysiwygSetup.prototype.originalGetSettings = tinyMceWysiwygSetup.prototype.getSettings;
tinyMceWysiwygSetup.prototype.getSettings = function(mode)
{
var settings = this.originalGetSettings(mode);
//add any extra settings you'd like below
//makes "placeholder" a valid element for inputs
settings.extended_valid_elements = 'input[placeholder|accept|alt|checked|disabled|maxlength|name|readonly|size|src|type|value]';
return settings;
}
Here’s what the code above does. First, we copy the Magento-javascript native getSettings
method to a new property named originalGetSettings
tinyMceWysiwygSetup.prototype.originalGetSettings = tinyMceWysiwygSetup.prototype.getSettings;
This will, in effect, give objects instantiated from this class a new method named originalGetSettings
, and this method will behave exactly the same as getSettings
. Next, we redefine the existing getSettings
method
tinyMceWysiwygSetup.prototype.getSettings = function(mode)
{
var settings = this.originalGetSettings(mode);
//add any extra settings you'd like below
...
}
and as the first line of our new method we call originalGetSetting
— in other words we call the original method. At this point, if we performed a return settings
, the wysiwygpage_content
would perform exactly the same tasks as it would prior to our modification.
All this leaves for us to do is add our new extended_valid_elements
value (which adds placeholder
as a valid input
element), and then return the entire settings
object.
settings.extended_valid_elements = 'input[placeholder|accept|alt|checked|disabled|maxlength|name|readonly|size|src|type|value]';
return settings;
All in all, the total patch less than 15 additional lines of Javascript.
if(window.tinyMceWysiwygSetup)
{
tinyMceWysiwygSetup.prototype.originalGetSettings = tinyMceWysiwygSetup.prototype.getSettings;
tinyMceWysiwygSetup.prototype.getSettings = function(mode)
{
var settings = this.originalGetSettings(mode);
//add any extra settings you'd like below
//makes "placeholder" a valid element for inputs
settings.extended_valid_elements = 'input[placeholder|accept|alt|checked|disabled|maxlength|name|readonly|size|src|type|value]';
return settings;
}
}
While we’d all like Magento itself to provide an officially blessed fix, a patch this small seems like an acceptable compromise.
Wrap Up
While The Industry™ trade press is filled weekly with announcements of new javascript frameworks and new WYSIWYG editors, unless you’re working an 80 hours a week startup job that lets you pick your own technology, chances are you’re going to interact with older, less hip technologies as part of your day job. If you’re planning on continuing along the programming path in your career, it’s important to understand the fundamentals of how these systems are built.
If you can shift your thinking from “Why is Thing X broken” to “How was Thing X built”, you’ll find yourself better able to address real world problems you and your team may have with the technology you use.