1 /***************************************************************
4 * (c) 2007-2012 Stanislas Rolland <typo3(arobas)sjbr.ca>
7 * This script is part of the TYPO3 project. The TYPO3 project is
8 * free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 * A copy is found in the textfile GPL.txt and important notices to the license
16 * from the author is found in LICENSE.txt distributed with these scripts.
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
28 * Text Style Plugin for TYPO3 htmlArea RTE
31 * Creation of the class of TextStyle plugins
33 HTMLArea
.TextStyle
= Ext
.extend(HTMLArea
.Plugin
, {
35 * This function gets called by the class constructor
37 configurePlugin: function (editor
) {
39 this.classesUrl
= this.editorConfiguration
.classesUrl
;
40 this.pageTSconfiguration
= this.editorConfiguration
.buttons
.textstyle
;
41 this.tags
= (this.pageTSconfiguration
&& this.pageTSconfiguration
.tags
) ? this.pageTSconfiguration
.tags
: {};
43 for (var tagName
in this.tags
) {
44 if (this.tags
.hasOwnProperty(tagName
)) {
45 if (this.tags
[tagName
].allowedClasses
) {
46 allowedClasses
= this.tags
[tagName
].allowedClasses
.trim().split(",");
47 for (var cssClass
in allowedClasses
) {
48 if (allowedClasses
.hasOwnProperty(cssClass
)) {
49 allowedClasses
[cssClass
] = allowedClasses
[cssClass
].trim().replace(/\*/g, ".*");
52 this.tags
[tagName
].allowedClasses
= new RegExp( "^(" + allowedClasses
.join("|") + ")$", "i");
56 this.showTagFreeClasses
= this.pageTSconfiguration
? this.pageTSconfiguration
.showTagFreeClasses
: false;
57 this.prefixLabelWithClassName
= this.pageTSconfiguration
? this.pageTSconfiguration
.prefixLabelWithClassName
: false;
58 this.postfixLabelWithClassName
= this.pageTSconfiguration
? this.pageTSconfiguration
.postfixLabelWithClassName
: false;
60 * Regular expression to check if an element is an inline elment
62 this.REInlineTags
= /^(a|abbr|acronym|b|bdo|big|cite|code|del|dfn|em|i|img|ins|kbd|q|samp|small|span|strike|strong|sub|sup|tt|u|var)$/;
64 // Allowed attributes on inline elements
65 this.allowedAttributes
= new Array('id', 'title', 'lang', 'xml:lang', 'dir', 'class', 'itemscope', 'itemtype', 'itemprop');
67 this.addAllowedAttribute('className');
70 * Registering plugin "About" information
72 var pluginInformation
= {
74 developer
: 'Stanislas Rolland',
75 developerUrl
: 'http://www.sjbr.ca/',
76 copyrightOwner
: 'Stanislas Rolland',
77 sponsor
: this.localize('Technische Universitat Ilmenau'),
78 sponsorUrl
: 'http://www.tu-ilmenau.de/',
81 this.registerPluginInformation(pluginInformation
);
83 * Registering the dropdown list
85 var buttonId
= 'TextStyle';
86 var fieldLabel
= this.pageTSconfiguration
? this.pageTSconfiguration
.fieldLabel
: '';
87 if (Ext
.isEmpty(fieldLabel
) && this.isButtonInToolbar('I[text_style]')) {
88 fieldLabel
= this.localize('text_style');
90 var dropDownConfiguration
= {
92 tooltip
: this.localize(buttonId
+ '-Tooltip'),
93 fieldLabel
: fieldLabel
,
94 options
: [[this.localize('No style'), 'none']],
96 storeFields
: [ { name
: 'text'}, { name
: 'value'}, { name
: 'style'} ],
97 tpl
: '<tpl for="."><div ext:qtip="{value}" style="{style}text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
99 if (this.pageTSconfiguration
) {
100 if (this.pageTSconfiguration
.width
) {
101 dropDownConfiguration
.width
= parseInt(this.pageTSconfiguration
.width
, 10);
103 if (this.pageTSconfiguration
.listWidth
) {
104 dropDownConfiguration
.listWidth
= parseInt(this.pageTSconfiguration
.listWidth
, 10);
106 if (this.pageTSconfiguration
.maxHeight
) {
107 dropDownConfiguration
.maxHeight
= parseInt(this.pageTSconfiguration
.maxHeight
, 10);
110 this.registerDropDown(dropDownConfiguration
);
113 isInlineElement: function (el
) {
114 return el
&& (el
.nodeType
=== HTMLArea
.DOM
.ELEMENT_NODE
) && this.REInlineTags
.test(el
.nodeName
.toLowerCase());
117 * This function adds an attribute to the array of allowed attributes on inline elements
119 * @param string attribute: the name of the attribute to be added to the array
123 addAllowedAttribute: function (attribute
) {
124 this.allowedAttributes
.push(attribute
);
127 * This function gets called when some style in the drop-down list applies it to the highlighted textt
129 onChange: function (editor
, combo
, record
, index
) {
130 var className
= combo
.getValue();
131 var classNames
= null;
132 var fullNodeSelected
= false;
133 var statusBarSelection
= this.editor
.statusBar
? this.editor
.statusBar
.getSelection() : null;
134 var range
= this.editor
.getSelection().createRange();
135 var parent
= this.editor
.getSelection().getParentElement();
136 var selectionEmpty
= this.editor
.getSelection().isEmpty();
137 var ancestors
= this.editor
.getSelection().getAllAncestors();
139 if (!selectionEmpty
) {
140 // The selection is not empty
141 for (var i
= 0; i
< ancestors
.length
; ++i
) {
142 fullNodeSelected
= (Ext
.isIE
&& ((statusBarSelection
=== ancestors
[i
] && ancestors
[i
].innerText
=== range
.text
) || (!statusBarSelection
&& ancestors
[i
].innerText
=== range
.text
)))
143 || (!Ext
.isIE
&& ((statusBarSelection
=== ancestors
[i
] && ancestors
[i
].textContent
=== range
.toString()) || (!statusBarSelection
&& ancestors
[i
].textContent
=== range
.toString())));
144 if (fullNodeSelected
) {
145 if (this.isInlineElement(ancestors
[i
])) {
146 parent
= ancestors
[i
];
151 // Working around bug in Safari selectNodeContents
152 if (!fullNodeSelected
&& Ext
.isWebKit
&& statusBarSelection
&& this.isInlineElement(statusBarSelection
) && statusBarSelection
.textContent
=== range
.toString()) {
153 fullNodeSelected
= true;
154 parent
= statusBarSelection
;
157 if (!selectionEmpty
&& !fullNodeSelected
|| (!selectionEmpty
&& fullNodeSelected
&& parent
&& HTMLArea
.DOM
.isBlockElement(parent
))) {
158 // The selection is not empty, nor full element, or the selection is full block element
159 if (className
!== "none") {
160 // Add span element with class attribute
161 var newElement
= editor
.document
.createElement('span');
162 HTMLArea
.DOM
.addClass(newElement
, className
);
163 editor
.getDomNode().wrapWithInlineElement(newElement
, range
);
169 this.applyClassChange(parent
, className
);
173 * This function applies the class change to the node
175 * @param object node: the node on which to apply the class change
176 * @param string className: the class to add, 'none' to remove the last class added to the class attribute
177 * @param boolean noRemove: true not to remove a span element with no more attribute
181 applyClassChange: function (node
, className
, noRemove
) {
182 // Add or remove class
183 if (node
&& !HTMLArea
.DOM
.isBlockElement(node
)) {
184 if (className
=== 'none' && node
.className
&& /\S/.test(node
.className
)) {
185 classNames
= node
.className
.trim().split(' ');
186 HTMLArea
.DOM
.removeClass(node
, classNames
[classNames
.length
-1]);
188 if (className
!== 'none') {
189 HTMLArea
.DOM
.addClass(node
, className
);
191 // Remove the span tag if it has no more attribute
192 if (/^span$/i.test(node
.nodeName
) && !HTMLArea
.DOM
.hasAllowedAttributes(node
, this.allowedAttributes
) && !noRemove
) {
193 this.editor
.getDomNode().removeMarkup(node
);
198 * This function gets called when the plugin is generated
199 * Get the classes configuration and initiate the parsing of the style sheets
201 onGenerate: function () {
202 // Monitor editor changing mode
203 this.editor
.iframe
.mon(this.editor
, 'HTMLAreaEventModeChange', this.onModeChange
, this);
204 // Create CSS Parser object
205 this.textStyles
= new HTMLArea
.CSS
.Parser({
206 prefixLabelWithClassName
: this.prefixLabelWithClassName
,
207 postfixLabelWithClassName
: this.postfixLabelWithClassName
,
208 showTagFreeClasses
: this.showTagFreeClasses
,
212 // Disable the combo while initialization completes
213 var dropDown
= this.getButton('TextStyle');
215 dropDown
.setDisabled(true);
217 // Monitor css parsing being completed
218 this.editor
.iframe
.mon(this.textStyles
, 'HTMLAreaEventCssParsingComplete', this.onCssParsingComplete
, this);
219 this.textStyles
.initiateParsing();
222 * This handler gets called when parsing of css classes is completed
224 onCssParsingComplete: function () {
225 if (this.textStyles
.isReady
) {
226 this.cssArray
= this.textStyles
.getClasses();
227 if (this.getEditorMode() === 'wysiwyg' && this.editor
.isEditable()) {
228 this.updateToolbar('TextStyle');
233 * This handler gets called when the toolbar is being updated
235 onUpdateToolbar: function (button
, mode
, selectionEmpty
, ancestors
) {
236 if (mode
=== 'wysiwyg' && this.editor
.isEditable() && this.textStyles
.isReady
) {
237 this.updateToolbar(button
.itemId
);
241 * This handler gets called when the editor has changed its mode to "wysiwyg"
243 onModeChange: function (mode
) {
244 if (mode
=== 'wysiwyg' && this.editor
.isEditable()) {
245 this.updateToolbar('TextStyle');
249 * This function gets called when the drop-down list needs to be refreshed
251 updateToolbar: function (dropDownId
) {
252 var editor
= this.editor
;
253 if (this.getEditorMode() === "wysiwyg" && this.editor
.isEditable()) {
254 var tagName
= false, classNames
= Array(), fullNodeSelected
= false;
255 var statusBarSelection
= editor
.statusBar
? editor
.statusBar
.getSelection() : null;
256 var range
= editor
.getSelection().createRange();
257 var parent
= editor
.getSelection().getParentElement();
258 var ancestors
= editor
.getSelection().getAllAncestors();
259 if (parent
&& !HTMLArea
.DOM
.isBlockElement(parent
)) {
260 tagName
= parent
.nodeName
.toLowerCase();
261 if (parent
.className
&& /\S/.test(parent
.className
)) {
262 classNames
= parent
.className
.trim().split(" ");
265 var selectionEmpty
= editor
.getSelection().isEmpty();
266 if (!selectionEmpty
) {
267 for (var i
= 0; i
< ancestors
.length
; ++i
) {
268 fullNodeSelected
= (statusBarSelection
=== ancestors
[i
])
269 && ((!Ext
.isIE
&& ancestors
[i
].textContent
=== range
.toString()) || (Ext
.isIE
&& ancestors
[i
].innerText
=== range
.text
));
270 if (fullNodeSelected
) {
271 if (!HTMLArea
.DOM
.isBlockElement(ancestors
[i
])) {
272 tagName
= ancestors
[i
].nodeName
.toLowerCase();
273 if (ancestors
[i
].className
&& /\S/.test(ancestors
[i
].className
)) {
274 classNames
= ancestors
[i
].className
.trim().split(" ");
280 // Working around bug in Safari selectNodeContents
281 if (!fullNodeSelected
&& Ext
.isWebKit
&& statusBarSelection
&& this.isInlineElement(statusBarSelection
) && statusBarSelection
.textContent
=== range
.toString()) {
282 fullNodeSelected
= true;
283 tagName
= statusBarSelection
.nodeName
.toLowerCase();
284 if (statusBarSelection
.className
&& /\S/.test(statusBarSelection
.className
)) {
285 classNames
= statusBarSelection
.className
.trim().split(" ");
289 var selectionInInlineElement
= tagName
&& this.REInlineTags
.test(tagName
);
290 var disabled
= !editor
.getSelection().endPointsInSameBlock() || (fullNodeSelected
&& !tagName
) || (selectionEmpty
&& !selectionInInlineElement
);
291 if (!disabled
&& !tagName
) {
294 this.updateValue(dropDownId
, tagName
, classNames
, selectionEmpty
, fullNodeSelected
, disabled
);
296 var dropDown
= this.getButton(dropDownId
);
298 dropDown
.setDisabled(!dropDown
.textMode
);
303 * This function reinitializes the options of the dropdown
305 initializeDropDown: function (dropDown
) {
306 var store
= dropDown
.getStore();
307 store
.removeAll(false);
308 store
.insert(0, new store
.recordType({
309 text
: this.localize('No style'),
312 dropDown
.setValue('none');
315 * This function builds the options to be displayed in the dropDown box
317 buildDropDownOptions: function (dropDown
, nodeName
) {
318 var store
= dropDown
.getStore();
319 this.initializeDropDown(dropDown
);
320 if (this.textStyles
.isReady
) {
321 var allowedClasses
= {};
322 if (this.REInlineTags
.test(nodeName
)) {
323 if (Ext
.isDefined(this.cssArray
[nodeName
])) {
324 allowedClasses
= this.cssArray
[nodeName
];
325 } else if (this.showTagFreeClasses
&& Ext
.isDefined(this.cssArray
['all'])) {
326 allowedClasses
= this.cssArray
['all'];
329 Ext
.iterate(allowedClasses
, function (cssClass
, value
) {
330 store
.add(new store
.recordType({
333 style
: (!(this.pageTSconfiguration
&& this.pageTSconfiguration
.disableStyleOnOptionLabel
) && HTMLArea
.classesValues
&& HTMLArea
.classesValues
[cssClass
] && !HTMLArea
.classesNoShow
[cssClass
]) ? HTMLArea
.classesValues
[cssClass
] : null
339 * This function sets the selected option of the dropDown box
341 setSelectedOption: function (dropDown
, classNames
, noUnknown
, defaultClass
) {
342 var store
= dropDown
.getStore();
343 dropDown
.setValue('none');
344 if (classNames
.length
) {
345 var index
= store
.findExact('value', classNames
[classNames
.length
-1]);
347 dropDown
.setValue(classNames
[classNames
.length
-1]);
349 store
.getAt(0).set('text', this.localize('Remove style'));
352 if (index
== -1 && !noUnknown
) {
353 store
.add(new store
.recordType({
354 text
: this.localize('Unknown style'),
355 value
: classNames
[classNames
.length
-1]
357 index
= store
.getCount()-1;
358 dropDown
.setValue(classNames
[classNames
.length
-1]);
360 store
.getAt(0).set('text', this.localize('Remove style'));
363 // Remove already assigned classes from the dropDown box
364 var classNamesString
= ',' + classNames
.join(',') + ',';
365 store
.each(function (option
) {
366 if (classNamesString
.indexOf("," + option
.get('value') + ",") != -1) {
367 store
.removeAt(store
.indexOf(option
));
372 dropDown
.setDisabled(!store
.getCount() || (store
.getCount() == 1 && dropDown
.getValue() == 'none'));
375 * This function updates the current value of the dropdown list
377 updateValue: function (dropDownId
, nodeName
, classNames
, selectionEmpty
, fullNodeSelected
, disabled
) {
378 var editor
= this.editor
;
379 var dropDown
= this.getButton(dropDownId
);
381 this.buildDropDownOptions(dropDown
, nodeName
);
382 if (classNames
.length
&& (selectionEmpty
|| fullNodeSelected
)) {
383 this.setSelectedOption(dropDown
, classNames
);
385 var store
= dropDown
.getStore();
386 dropDown
.setDisabled(!store
.getCount() || (store
.getCount() == 1 && dropDown
.getValue() == 'none') || disabled
);