30d5b8931e4dd2bb329dcc52b32404a189756c33
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / Plugins / EditElement.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14 /**
15 * EditElement plugin for htmlArea RTE
16 */
17 define([
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
21 'TYPO3/CMS/Rtehtmlarea/Plugins/MicrodataSchema',
22 'TYPO3/CMS/Rtehtmlarea/Plugins/Language',
23 'TYPO3/CMS/Rtehtmlarea/Plugins/BlockStyle',
24 'TYPO3/CMS/Rtehtmlarea/Plugins/TextStyle',
25 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Components/Select',
26 'jquery',
27 'TYPO3/CMS/Backend/Modal',
28 'TYPO3/CMS/Backend/Severity'
29 ], function (Plugin, Util, Dom, MicrodataSchema, Language, BlockStyle, TextStyle, Select, $, Modal, Severity) {
30
31 var EditElement = function (editor, pluginName) {
32 this.constructor.super.call(this, editor, pluginName);
33 };
34 Util.inherit(EditElement, Plugin);
35 Util.apply(EditElement.prototype, {
36
37 /**
38 * This function gets called by the class constructor
39 */
40 configurePlugin: function (editor) {
41 this.pageTSConfiguration = this.editorConfiguration.buttons.editelement;
42 this.removedFieldsets = (this.pageTSConfiguration && this.pageTSConfiguration.removeFieldsets) ? this.pageTSConfiguration.removeFieldsets : '';
43 this.properties = (this.pageTSConfiguration && this.pageTSConfiguration.properties) ? this.pageTSConfiguration.properties : '';
44 this.removedProperties = (this.properties && this.properties.removed) ? this.properties.removed : '';
45
46 /**
47 * Registering plugin "About" information
48 */
49 var pluginInformation = {
50 version : '2.0',
51 developer : 'Stanislas Rolland',
52 developerUrl : 'http://www.sjbr.ca/',
53 copyrightOwner : 'Stanislas Rolland',
54 sponsor : 'SJBR',
55 sponsorUrl : 'http://www.sjbr.ca/',
56 license : 'GPL'
57 };
58 this.registerPluginInformation(pluginInformation);
59
60 /**
61 * Registering the button
62 */
63 var buttonId = 'EditElement';
64 var buttonConfiguration = {
65 id : buttonId,
66 tooltip : this.localize('editElement'),
67 action : 'onButtonPress',
68 dialog : true,
69 iconCls : 'htmlarea-action-element-edit'
70 };
71 this.registerButton(buttonConfiguration);
72 return true;
73 },
74 /**
75 * Sets of default configuration values for dialogue form fields
76 */
77 configDefaults: {
78 combo: {
79 editable: true,
80 selectOnFocus: true,
81 typeAhead: true,
82 triggerAction: 'all',
83 forceSelection: true,
84 mode: 'local',
85 valueField: 'value',
86 displayField: 'text',
87 helpIcon: true,
88 tpl: '<tpl for="."><div ext:qtip="{value}" style="text-align:left;font-size:11px;" class="x-combo-list-item">{text}</div></tpl>'
89 }
90 },
91 /**
92 * This function gets called when the button was pressed
93 *
94 * @param {Object} editor The editor instance
95 * @param {String} id The button id or the key
96 * @return {Boolean} False if action is completed
97 */
98 onButtonPress: function(editor, id) {
99 // Could be a button or its hotkey
100 var buttonId = this.translateHotKey(id);
101 buttonId = buttonId ? buttonId : id;
102 // Get the parent element of the current selection
103 this.element = this.editor.getSelection().getParentElement();
104 if (this.element && !/^body$/i.test(this.element.nodeName)) {
105 // Open the dialogue window
106 this.openDialogue(
107 buttonId,
108 'editElement',
109 this.buildTabItemsConfig(this.element),
110 this.buildButtonsConfig(this.element, this.okHandler, this.deleteHandler)
111 );
112 }
113 return false;
114 },
115 /**
116 * Open the dialogue window
117 *
118 * @param {String} buttonId The button id
119 * @param {String} title The window title
120 * @param {Object} $tabItems The configuration of the tabbed panel
121 * @param {Object} buttonsConfig The configuration of the buttons
122 */
123 openDialogue: function (buttonId, title, $tabItems, buttonsConfig) {
124 this.dialog = Modal.show(this.localize(title), $tabItems, Severity.notice, buttonsConfig);
125 this.dialog.on('modal-dismiss', $.proxy(this.onClose, this));
126 },
127 /**
128 * Build the dialogue tab items config
129 *
130 * @param {Object} element The element being edited, if any
131 * @return {Object} The tab items configuration
132 */
133 buildTabItemsConfig: function (element) {
134 var $tabs = $('<ul />', {'class': 'nav nav-tabs', role: 'tablist'}),
135 $tabContent = $('<div />', {'class': 'tab-content'}),
136 $finalMarkup,
137 generalTabItemConfig = [],
138 languageTabItemConfig = [],
139 microdataTabItemConfig = [],
140 eventsTabItemConfig = [];
141
142 if (this.removedFieldsets.indexOf('identification') === -1) {
143 this.addConfigElement(this.buildIdentificationFieldsetConfig(element), generalTabItemConfig);
144 }
145 if (this.removedFieldsets.indexOf('style') === -1 && this.removedProperties.indexOf('className') === -1) {
146 this.addConfigElement(this.buildClassFieldsetConfig(element), generalTabItemConfig);
147 }
148
149 if (generalTabItemConfig.length > 0) {
150 this.buildTabMarkup($tabs, $tabContent, 'general', generalTabItemConfig, this.localize('general'));
151 }
152 if (this.removedFieldsets.indexOf('language') === -1 && this.getPluginInstance('Language')) {
153 this.addConfigElement(this.buildLanguageFieldsetConfig(element), languageTabItemConfig);
154 this.buildTabMarkup($tabs, $tabContent, 'language', languageTabItemConfig, this.localize('Language'));
155 }
156 if (this.removedFieldsets.indexOf('microdata') === -1 && this.getPluginInstance('MicrodataSchema')) {
157 this.addConfigElement(this.getPluginInstance('MicrodataSchema').buildMicrodataFieldsetConfig(element, this.properties), microdataTabItemConfig);
158 this.buildTabMarkup($tabs, $tabContent, 'microdata', microdataTabItemConfig, this.getPluginInstance('MicrodataSchema').localize('microdata'));
159 }
160 if (this.removedFieldsets.indexOf('events') === -1) {
161 this.addConfigElement(this.buildEventsFieldsetConfig(element), eventsTabItemConfig);
162 this.buildTabMarkup($tabs, $tabContent, 'events', eventsTabItemConfig, this.localize('events'));
163 }
164
165 $tabs.find('li:first').addClass('active');
166 $tabContent.find('.tab-pane:first').addClass('active');
167
168 $finalMarkup = $('<form />', {'class': 'form-horizontal'}).append($tabs, $tabContent);
169
170 return $finalMarkup;
171 },
172 /**
173 * This function builds the configuration object for the Identification fieldset
174 *
175 * @param {Object} element The element being edited, if any
176 * @return {Object} The fieldset configuration object
177 */
178 buildIdentificationFieldsetConfig: function (element) {
179 var $fieldset = $('<fieldset />');
180
181 $fieldset.append(
182 $('<h4 />', {'class': 'form-section-headline'}).text(this.localize('identification'))
183 );
184
185 if (this.removedProperties.indexOf('id') === -1) {
186 $fieldset.append(
187 $('<div />', {'class': 'form-group'}).append(
188 $('<label />', {'class': 'col-sm-2'}).html(this.getHelpTip('id', 'id')),
189 $('<div />', {'class': 'col-sm-10'}).append(
190 $('<input />', {name: 'id', 'class': 'form-control', value: element ? element.getAttribute('id') : ''})
191 )
192 )
193 );
194 }
195 if (this.removedProperties.indexOf('title') === -1) {
196 $fieldset.append(
197 $('<div />', {'class': 'form-group'}).append(
198 $('<label />', {'class': 'col-sm-2'}).html(this.getHelpTip('title', 'title')),
199 $('<div />', {'class': 'col-sm-10'}).append(
200 $('<input />', {name: 'title', 'class': 'form-control', value: element ? element.getAttribute('title') : ''})
201 )
202 )
203 );
204 }
205
206 return $fieldset;
207 },
208 /**
209 * This function builds the configuration object for the CSS Class fieldset
210 *
211 * @param {Object} element The element being edited, if any
212 * @return {Object} The fieldset configuration object
213 */
214 buildClassFieldsetConfig: function (element) {
215 var $fieldset = $('<fieldset />'),
216 stylingCombo = new Select(this.buildStylingField('className', 'className', 'className'));
217
218 $fieldset.append(
219 $('<h4 />', {'class': 'form-section-headline'}).text(this.localize('className'))
220 );
221
222 stylingCombo.render($fieldset[0]);
223 this.setStyleOptions(stylingCombo, element);
224
225 return $fieldset;
226 },
227 /**
228 * This function builds a style selection field
229 *
230 * @param {String} fieldName The name of the field
231 * @param {String} fieldLabel The label for the field
232 * @param {String} cshKey The csh key
233 * @return {Object} The style selection field object
234 */
235 buildStylingField: function (fieldName, fieldLabel, cshKey) {
236 // This is a nasty hack to fake ExtJS object configuration
237 return Util.apply(
238 {
239 xtype: 'htmlareaselect',
240 itemId: fieldName,
241 fieldLabel: this.getHelpTip(fieldLabel, cshKey),
242 helpTitle: typeof TYPO3.ContextHelp !== 'undefined' ? '' : this.localize(fieldTitle),
243 width: ((this.properties['className'] && this.properties['className'].width) ? this.properties['className'].width : 300)
244 },
245 this.configDefaults['combo']
246 );
247 },
248 /**
249 * This function populates the class store and sets the selected option
250 *
251 * @param {Object} comboBox The combobox object
252 * @param {Object} element The element being edited, if any
253 * @return {Object} The fieldset configuration object
254 */
255 setStyleOptions: function (comboBox, element) {
256 var nodeName = element.nodeName.toLowerCase();
257 this.stylePlugin = this.getPluginInstance(Dom.isBlockElement(element) ? 'BlockStyle' : 'TextStyle');
258 if (comboBox && this.stylePlugin) {
259 var classNames = Dom.getClassNames(element);
260 this.stylePlugin.buildDropDownOptions(comboBox, nodeName);
261 this.stylePlugin.setSelectedOption(comboBox, classNames);
262 }
263 },
264 /**
265 * This function builds the configuration object for the Language fieldset
266 *
267 * @param {Object} element The element being edited, if any
268 * @return {Object} The fieldset configuration object
269 */
270 buildLanguageFieldsetConfig: function (element) {
271 var self = this,
272 $fieldset = $('<fieldset />', {id: 'languageFieldset'}),
273 languagePlugin = this.getPluginInstance('Language'),
274 languageConfigurationUrl;
275
276 $fieldset.append(
277 $('<h4 />', {'class': 'form-section-headline'}).text(self.localize('Language'))
278 );
279
280 if (this.editorConfiguration.buttons && this.editorConfiguration.buttons.language && this.editorConfiguration.buttons.language.dataUrl) {
281 languageConfigurationUrl = this.editorConfiguration.buttons.language.dataUrl;
282 }
283 if (languagePlugin && languageConfigurationUrl && this.removedProperties.indexOf('language') === -1) {
284 var selectedLanguage = typeof element === 'object' && element !== null ? languagePlugin.getLanguageAttribute(element) : 'none';
285
286 $fieldset.append(
287 $('<div />', {'class': 'form-group'}).append(
288 $('<label />', {'class': 'col-sm-2'}).html(languagePlugin.getHelpTip('languageCombo', 'Language')),
289 $('<div />', {'class': 'col-sm-10'}).append(
290 $('<select />', {name: 'lang', 'class': 'form-control'})
291 )
292 )
293 );
294
295 $.ajax({
296 url: this.getDropDownConfiguration('Language').dataUrl,
297 dataType: 'json',
298 success: function (response) {
299 var $select = $fieldset.find('select[name="lang"]');
300
301 for (var language in response.options) {
302 if (response.options.hasOwnProperty(language)) {
303 if (selectedLanguage !== 'none') {
304 response.options[language].value = 'none';
305 response.options[language].text = languageObject.localize('Remove language mark');
306 }
307 var attributeConfiguration = {value: response.options[language].value};
308 if (selectedLanguage === response.options[language].value) {
309 attributeConfiguration.selected = 'selected';
310 }
311 $select.append(
312 $('<option />', attributeConfiguration).text(response.options[language].text)
313 );
314 }
315 }
316 }
317 });
318 }
319 if (this.removedProperties.indexOf('direction') === -1) {
320 $fieldset = this.attachSelectMarkup(
321 $fieldset,
322 languagePlugin.getHelpTip('directionCombo', 'Text direction'),
323 'dir',
324 [
325 [languagePlugin.localize('Not set'), 'not set'],
326 [languagePlugin.localize('RightToLeft'), 'rtl'],
327 [languagePlugin.localize('LeftToRight'), 'ltr']
328 ],
329 typeof element === 'object' && element !== null && element.dir ? element.dir : 'not set'
330 );
331 }
332 return $fieldset;
333 },
334 /**
335 * This function builds the configuration object for the Events fieldset
336 *
337 * @param {Object} element The element being edited, if any
338 *
339 * @return {Object} The fieldset configuration object
340 */
341 buildEventsFieldsetConfig: function (element) {
342 var $fieldset = $('<fieldset />');
343 var events = ['onkeydown', 'onkeypress', 'onkeyup', 'onclick', 'ondblclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup'];
344 if (!/^(base|bdo|br|frame|frameset|head|html|iframe|meta|param|script|style|title)$/i.test(element.nodeName)) {
345 var event;
346 for (var i = 0, n = events.length; i < n; i++) {
347 event = events[i];
348 if (this.removedProperties.indexOf(event) === -1) {
349 $fieldset.append(
350 $('<div />', {'class': 'form-group'}).append(
351 $('<label />', {'class': 'col-sm-3'}).html(this.getHelpTip(event, event)),
352 $('<div />', {'class': 'col-sm-9'}).append(
353 $('<input />', {name: event, 'class': 'form-control', value: element ? element.getAttribute(event) : ''})
354 )
355 )
356 );
357 }
358 }
359 }
360
361 return $fieldset;
362 },
363 /**
364 * Build the dialogue buttons config
365 *
366 * @param {Object} element The element being edited, if any
367 * @param {Function} okHandler The handler for the ok button
368 * @param {Function} deleteHandler The handler for the delete button
369 * @return {Object} The buttons configuration
370 */
371 buildButtonsConfig: function (element, okHandler, deleteHandler) {
372 var buttonsConfig = [];
373 buttonsConfig.push(this.buildButtonConfig('Cancel', $.proxy(this.onCancel, this), true));
374 if (element) {
375 buttonsConfig.push(this.buildButtonConfig('Delete', $.proxy(deleteHandler, this)));
376 }
377 buttonsConfig.push(this.buildButtonConfig('OK', $.proxy(okHandler, this), false, Severity.notice));
378 return buttonsConfig;
379 },
380 /**
381 * Handler when the ok button is pressed
382 *
383 * @param {Event} e
384 */
385 okHandler: function (e) {
386 this.restoreSelection();
387 var textFields = this.dialog.find('input');
388 for (var i = textFields.length; --i >= 0;) {
389 var field = $(textFields[i]),
390 value = field.val();
391 if (value) {
392 this.element.setAttribute(field.attr('name'), value);
393 } else {
394 this.element.removeAttribute(field.attr('name'));
395 }
396 }
397 var comboFields = this.dialog.find('select');
398 var languageCombo = this.dialog.find('[name="lang"]'),
399 languageComboValue = languageCombo.val();
400 for (var i = comboFields.length; --i >= 0;) {
401 var field = $(comboFields[i]),
402 itemId = field.attr('name'),
403 value = field.val();
404 switch (itemId) {
405 case 'className':
406 if (Dom.isBlockElement(this.element)) {
407 this.stylePlugin.applyClassChange(this.element, value);
408 } else {
409 // Do not remove the span element if the language attribute is to be removed
410 this.stylePlugin.applyClassChange(this.element, value, languageCombo && languageComboValue === 'none');
411 }
412 break;
413 case 'dir':
414 this.element.setAttribute(itemId, value === 'not set' ? '' : value);
415 break;
416 }
417 }
418 var microdataTab = this.dialog.find('[role="tab"] [href="#microdata"]');
419 if (microdataTab) {
420 this.getPluginInstance('MicrodataSchema').setMicrodataAttributes(this.element);
421 }
422 if (languageCombo) {
423 this.getPluginInstance('Language').setLanguageAttributes(this.element, languageComboValue);
424 }
425 Modal.currentModal.trigger('modal-dismiss');
426 e.stopImmediatePropagation();
427 },
428 /**
429 * Handler when the delete button is pressed
430 */
431 deleteHandler: function (button, event) {
432 this.restoreSelection();
433 if (this.element) {
434 // Delete the element
435 Dom.removeFromParent(this.element);
436 }
437 this.close();
438 event.stopEvent();
439 },
440 /**
441 * This function gets called when the toolbar is updated
442 */
443 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
444 if (mode === 'wysiwyg' && this.editor.isEditable()) {
445 // Disable the button if the first ancestor is the document body
446 button.setDisabled(!ancestors.length || /^body$/i.test(ancestors[0].nodeName));
447 if (this.dialog) {
448 this.dialog.focus();
449 }
450 }
451 }
452 });
453
454 return EditElement;
455 });