1 /***************************************************************
4 * (c) 2007-2010 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
33 * Creation of the class of TextStyle plugins
35 HTMLArea
.TextStyle
= HTMLArea
.Plugin
.extend({
37 * Let the base class do some initialization work
39 constructor: function (editor
, pluginName
) {
40 this.base(editor
, pluginName
);
43 * This function gets called by the class constructor
45 configurePlugin: function (editor
) {
47 this.classesUrl
= this.editorConfiguration
.classesUrl
;
48 this.pageTSconfiguration
= this.editorConfiguration
.buttons
.textstyle
;
49 this.tags
= this.pageTSconfiguration
.tags
;
51 this.tags
= new Object();
53 if (typeof(this.editorConfiguration
.classesTag
) !== "undefined") {
54 if (this.editorConfiguration
.classesTag
.span
) {
55 if (!this.tags
.span
) {
56 this.tags
.span
= new Object();
58 if (!this.tags
.span
.allowedClasses
) {
59 this.tags
.span
.allowedClasses
= this.editorConfiguration
.classesTag
.span
;
64 for (var tagName
in this.tags
) {
65 if (this.tags
.hasOwnProperty(tagName
)) {
66 if (this.tags
[tagName
].allowedClasses
) {
67 allowedClasses
= this.tags
[tagName
].allowedClasses
.trim().split(",");
68 for (var cssClass
in allowedClasses
) {
69 if (allowedClasses
.hasOwnProperty(cssClass
)) {
70 allowedClasses
[cssClass
] = allowedClasses
[cssClass
].trim().replace(/\*/g, ".*");
73 this.tags
[tagName
].allowedClasses
= new RegExp( "^(" + allowedClasses
.join("|") + ")$", "i");
77 this.showTagFreeClasses
= this.pageTSconfiguration
.showTagFreeClasses
|| this.editorConfiguration
.showTagFreeClasses
;
78 this.prefixLabelWithClassName
= this.pageTSconfiguration
.prefixLabelWithClassName
;
79 this.postfixLabelWithClassName
= this.pageTSconfiguration
.postfixLabelWithClassName
;
82 * Regular expression to check if an element is an inline elment
84 this.REInlineTags
= /^(abbr|acronym|b|bdo|big|cite|code|del|dfn|em|i|ins|kbd|q|samp|small|span|strike|strong|sub|sup|tt|u|var)$/;
86 // Allowed attributes on inline elements
87 this.allowedAttributes
= new Array("id", "title", "lang", "xml:lang", "dir", "class");
89 this.addAllowedAttribute("className");
92 * Registering plugin "About" information
94 var pluginInformation
= {
96 developer
: 'Stanislas Rolland',
97 developerUrl
: 'http://www.sjbr.ca/',
98 copyrightOwner
: 'Stanislas Rolland',
99 sponsor
: this.localize('Technische Universitat Ilmenau'),
100 sponsorUrl
: 'http://www.tu-ilmenau.de/',
103 this.registerPluginInformation(pluginInformation
);
105 * Registering the dropdown list
107 var buttonId
= 'TextStyle';
108 var fieldLabel
= this.pageTSconfiguration
.fieldLabel
;
109 if (Ext
.isEmpty(fieldLabel
) && this.isButtonInToolbar('I[text_style]')) {
110 fieldLabel
= this.localize('text_style');
112 var dropDownConfiguration
= {
114 tooltip
: this.localize(buttonId
+ '-Tooltip'),
115 fieldLabel
: fieldLabel
,
116 options
: [[this.localize('No style'), 'none']],
118 storeFields
: [ { name
: 'text'}, { name
: 'value'}, { name
: 'style'} ],
119 tpl
: '<tpl for="."><div ext:qtip="{value}" style="{style}text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
121 if (this.pageTSconfiguration
.width
) {
122 dropDownConfiguration
.width
= parseInt(this.pageTSconfiguration
.width
, 10);
124 if (this.pageTSconfiguration
.listWidth
) {
125 dropDownConfiguration
.listWidth
= parseInt(this.pageTSconfiguration
.listWidth
, 10);
127 if (this.pageTSconfiguration
.maxHeight
) {
128 dropDownConfiguration
.maxHeight
= parseInt(this.pageTSconfiguration
.maxHeight
, 10);
130 this.registerDropDown(dropDownConfiguration
);
134 isInlineElement: function (el
) {
135 return el
&& (el
.nodeType
=== 1) && this.REInlineTags
.test(el
.nodeName
.toLowerCase());
138 * This function adds an attribute to the array of allowed attributes on inline elements
140 * @param string attribute: the name of the attribute to be added to the array
144 addAllowedAttribute: function (attribute
) {
145 this.allowedAttributes
.push(attribute
);
148 * This function gets called when some style in the drop-down list applies it to the highlighted textt
150 onChange: function (editor
, combo
, record
, index
) {
151 var className
= combo
.getValue();
152 var classNames
= null;
153 var fullNodeSelected
= false;
156 var selection
= this.editor
._getSelection();
157 var statusBarSelection
= this.editor
.statusBar
? this.editor
.statusBar
.getSelection() : null;
158 var range
= this.editor
._createRange(selection
);
159 var parent
= this.editor
.getParentElement();
160 var selectionEmpty
= this.editor
._selectionEmpty(selection
);
161 var ancestors
= this.editor
.getAllAncestors();
163 var bookmark
= range
.getBookmark();
166 if (!selectionEmpty
) {
167 // The selection is not empty
168 for (var i
= 0; i
< ancestors
.length
; ++i
) {
169 fullNodeSelected
= (Ext
.isIE
&& ((statusBarSelection
=== ancestors
[i
] && ancestors
[i
].innerText
=== range
.text
) || (!statusBarSelection
&& ancestors
[i
].innerText
=== range
.text
)))
170 || (!Ext
.isIE
&& ((statusBarSelection
=== ancestors
[i
] && ancestors
[i
].textContent
=== range
.toString()) || (!statusBarSelection
&& ancestors
[i
].textContent
=== range
.toString())));
171 if (fullNodeSelected
) {
172 if (this.isInlineElement(ancestors
[i
])) {
173 parent
= ancestors
[i
];
178 // Working around bug in Safari selectNodeContents
179 if (!fullNodeSelected
&& Ext
.isWebKit
&& statusBarSelection
&& this.isInlineElement(statusBarSelection
) && statusBarSelection
.textContent
=== range
.toString()) {
180 fullNodeSelected
= true;
181 parent
= statusBarSelection
;
184 if (!selectionEmpty
&& !fullNodeSelected
|| (!selectionEmpty
&& fullNodeSelected
&& parent
&& HTMLArea
.isBlockElement(parent
))) {
185 // The selection is not empty, nor full element, or the selection is full block element
186 if (className
!== "none") {
187 // Add span element with class attribute
188 var newElement
= editor
._doc
.createElement("span");
189 HTMLArea
.DOM
.addClass(newElement
, className
);
190 editor
.wrapWithInlineElement(newElement
, selection
, range
);
196 // Add or remove class
197 if (parent
&& !HTMLArea
.isBlockElement(parent
)) {
198 if (className
=== "none" && parent
.className
&& /\S/.test(parent
.className
)) {
199 classNames
= parent
.className
.trim().split(" ");
200 HTMLArea
.DOM
.removeClass(parent
, classNames
[classNames
.length
-1]);
202 if (className
!== "none") {
203 HTMLArea
.DOM
.addClass(parent
, className
);
205 // Remove the span tag if it has no more attribute
206 if ((parent
.nodeName
.toLowerCase() === "span") && !HTMLArea
.hasAllowedAttributes(parent
, this.allowedAttributes
)) {
207 editor
.removeMarkup(parent
);
213 * This function gets called when the plugin is generated
214 * Get the classes configuration and initiate the parsing of the style sheets
216 onGenerate: function () {
217 // Monitor editor changing mode
218 this.editor
.iframe
.mon(this.editor
, 'HTMLAreaEventModeChange', this.onModeChange
, this);
219 // Create CSS Parser object
220 this.textStyles
= new HTMLArea
.CSS
.Parser({
221 prefixLabelWithClassName
: this.prefixLabelWithClassName
,
222 postfixLabelWithClassName
: this.postfixLabelWithClassName
,
223 showTagFreeClasses
: this.showTagFreeClasses
,
227 // Monitor css parsing being completed
228 this.editor
.iframe
.mon(this.textStyles
, 'HTMLAreaEventCssParsingComplete', this.onCssParsingComplete
, this);
229 this.textStyles
.initiateParsing();
232 * This handler gets called when parsing of css classes is completed
234 onCssParsingComplete: function () {
235 if (this.textStyles
.isReady
) {
236 this.cssArray
= this.textStyles
.getClasses();
238 if (this.getEditorMode() === 'wysiwyg' && this.editor
.isEditable()) {
239 this.updateToolbar('TextStyle');
243 * This handler gets called when the toolbar is being updated
245 onUpdateToolbar: function (button
, mode
, selectionEmpty
, ancestors
) {
246 if (mode
=== 'wysiwyg' && this.editor
.isEditable()) {
247 this.updateToolbar(button
.itemId
);
251 * This handler gets called when the editor has changed its mode to "wysiwyg"
253 onModeChange: function (mode
) {
254 if (mode
=== 'wysiwyg' && this.editor
.isEditable()) {
255 this.updateToolbar('TextStyle');
259 * This function gets called when the drop-down list needs to be refreshed
261 updateToolbar: function(dropDownId
) {
262 var editor
= this.editor
;
263 if (this.getEditorMode() === "wysiwyg" && this.editor
.isEditable()) {
264 var tagName
= false, classNames
= Array(), fullNodeSelected
= false;
265 var selection
= editor
._getSelection();
266 var statusBarSelection
= editor
.statusBar
? editor
.statusBar
.getSelection() : null;
267 var range
= editor
._createRange(selection
);
268 var parent
= editor
.getParentElement(selection
);
269 var ancestors
= editor
.getAllAncestors();
270 if (parent
&& !HTMLArea
.isBlockElement(parent
)) {
271 tagName
= parent
.nodeName
.toLowerCase();
272 if (parent
.className
&& /\S/.test(parent
.className
)) {
273 classNames
= parent
.className
.trim().split(" ");
276 var selectionEmpty
= editor
._selectionEmpty(selection
);
277 if (!selectionEmpty
) {
278 for (var i
= 0; i
< ancestors
.length
; ++i
) {
279 fullNodeSelected
= (statusBarSelection
=== ancestors
[i
])
280 && ((!Ext
.isIE
&& ancestors
[i
].textContent
=== range
.toString()) || (Ext
.isIE
&& ancestors
[i
].innerText
=== range
.text
));
281 if (fullNodeSelected
) {
282 if (!HTMLArea
.isBlockElement(ancestors
[i
])) {
283 tagName
= ancestors
[i
].nodeName
.toLowerCase();
284 if (ancestors
[i
].className
&& /\S/.test(ancestors
[i
].className
)) {
285 classNames
= ancestors
[i
].className
.trim().split(" ");
291 // Working around bug in Safari selectNodeContents
292 if (!fullNodeSelected
&& Ext
.isWebKit
&& statusBarSelection
&& this.isInlineElement(statusBarSelection
) && statusBarSelection
.textContent
=== range
.toString()) {
293 fullNodeSelected
= true;
294 tagName
= statusBarSelection
.nodeName
.toLowerCase();
295 if (statusBarSelection
.className
&& /\S/.test(statusBarSelection
.className
)) {
296 classNames
= statusBarSelection
.className
.trim().split(" ");
300 var selectionInInlineElement
= tagName
&& this.REInlineTags
.test(tagName
);
301 var disabled
= !editor
.endPointsInSameBlock() || (fullNodeSelected
&& !tagName
) || (selectionEmpty
&& !selectionInInlineElement
);
302 if (!disabled
&& !tagName
) {
305 this.updateValue(dropDownId
, tagName
, classNames
, selectionEmpty
, fullNodeSelected
, disabled
);
307 var dropDown
= this.getButton(dropDownId
);
309 dropDown
.setDisabled(!dropDown
.textMode
);
314 * This function reinitializes the options of the dropdown
316 initializeDropDown: function (dropDown
) {
317 var store
= dropDown
.getStore();
318 store
.removeAll(false);
319 store
.insert(0, new store
.recordType({
320 text
: this.localize('No style'),
323 dropDown
.setValue('none');
326 * This function sets the selected option of the dropDown box
328 setSelectedOption: function (dropDown
, classNames
, noUnknown
, defaultClass
) {
329 var store
= dropDown
.getStore();
330 var index
= store
.findExact('value', classNames
[classNames
.length
-1]);
332 dropDown
.setValue(classNames
[classNames
.length
-1]);
334 store
.getAt(0).set('text', this.localize('Remove style'));
337 if (index
== -1 && !noUnknown
) {
338 store
.add(new store
.recordType({
339 text
: this.localize('Unknown style'),
340 value
: classNames
[classNames
.length
-1]
342 index
= store
.getCount()-1;
343 dropDown
.setValue(classNames
[classNames
.length
-1]);
345 store
.getAt(0).set('text', this.localize('Remove style'));
348 store
.each(function (option
) {
349 if (("," + classNames
.join(",") + ",").indexOf("," + option
.get('value') + ",") != -1 && store
.indexOf(option
) != index
) {
350 store
.removeAt(store
.indexOf(option
));
356 * This function updates the current value of the dropdown list
358 updateValue: function (dropDownId
, nodeName
, classNames
, selectionEmpty
, fullNodeSelected
, disabled
) {
359 var editor
= this.editor
;
360 var dropDown
= this.getButton(dropDownId
);
362 var store
= dropDown
.getStore();
363 this.initializeDropDown(dropDown
);
364 if (this.textStyles
.isReady
) {
365 var allowedClasses
= {};
366 if (this.REInlineTags
.test(nodeName
)) {
367 if (Ext
.isDefined(this.cssArray
[nodeName
])) {
368 allowedClasses
= this.cssArray
[nodeName
];
369 } else if (this.showTagFreeClasses
&& Ext
.isDefined(this.cssArray
['all'])) {
370 allowedClasses
= this.cssArray
['all'];
373 Ext
.iterate(allowedClasses
, function (cssClass
, value
) {
374 store
.add(new store
.recordType({
377 style
: (!this.editor
.config
.disablePCexamples
&& HTMLArea
.classesValues
&& HTMLArea
.classesValues
[cssClass
] && !HTMLArea
.classesNoShow
[cssClass
]) ? HTMLArea
.classesValues
[cssClass
] : null
381 if (classNames
.length
&& (selectionEmpty
|| fullNodeSelected
)) {
382 this.setSelectedOption(dropDown
, classNames
);
384 dropDown
.setDisabled(!(store
.getCount()>1) || disabled
);