19363aed3f0efc9bfa028bf13a2afa1f9110b04f
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / Plugins / ContextMenu.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 * Context Menu Plugin for TYPO3 htmlArea RTE
16 */
17 define(['TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin',
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
21 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event'],
22 function (Plugin, UserAgent, Util, Dom, Event) {
23
24 var ContextMenu = function (editor, pluginName) {
25 this.constructor.super.call(this, editor, pluginName);
26 };
27 Util.inherit(ContextMenu, Plugin);
28 Util.apply(ContextMenu.prototype, {
29
30 /**
31 * This function gets called by the class constructor
32 */
33 configurePlugin: function(editor) {
34 this.pageTSConfiguration = this.editorConfiguration.contextMenu;
35 if (!this.pageTSConfiguration) {
36 this.pageTSConfiguration = {};
37 }
38 if (this.pageTSConfiguration.showButtons) {
39 this.showButtons = this.pageTSConfiguration.showButtons;
40 }
41 if (this.pageTSConfiguration.hideButtons) {
42 this.hideButtons = this.pageTSConfiguration.hideButtons;
43 }
44 /**
45 * Registering plugin "About" information
46 */
47 var pluginInformation = {
48 version : '3.2',
49 developer : 'Mihai Bazon & Stanislas Rolland',
50 developerUrl : 'http://www.sjbr.ca/',
51 copyrightOwner : 'dynarch.com & Stanislas Rolland',
52 sponsor : 'American Bible Society & SJBR',
53 sponsorUrl : 'http://www.sjbr.ca/',
54 license : 'GPL'
55 };
56 this.registerPluginInformation(pluginInformation);
57 return true;
58 },
59
60 /**
61 * This function gets called when the editor gets generated
62 */
63 onGenerate: function() {
64 var self = this;
65 // Build the context menu
66 this.menu = new Ext.menu.Menu(Util.applyIf({
67 cls: 'htmlarea-context-menu',
68 defaultType: 'menuitem',
69 maxHeight: this.editor.iframe.height - this.editor.document.documentElement.clientHeight,
70 listeners: {
71 itemClick: {
72 fn: this.onItemClick,
73 scope: this
74 },
75 show: {
76 fn: this.onShow,
77 scope: this
78 },
79 hide: {
80 fn: this.onHide,
81 scope: this
82 }
83 },
84 items: this.buildItemsConfig()
85 }, this.pageTSConfiguration));
86 // Monitor contextmenu clicks on the iframe
87 Event.on(this.editor.document.documentElement, 'contextmenu', function (event) { return self.show(event, event.target); });
88 // Monitor editor being unloaded
89 Event.one(this.editor.iframe.getIframeWindow(), 'unload', function (event) { self.onBeforeDestroy(event); return true; });
90
91 this.mousePosition = {
92 x: 0,
93 y: 0
94 };
95 var onMouseUpdate = function(e) {
96 self.mousePosition.x = e.pageX;
97 self.mousePosition.y = e.pageY;
98 };
99 Event.on(this.editor.document.documentElement, 'mousemove', onMouseUpdate);
100 Event.on(this.editor.document.documentElement, 'mouseenter', onMouseUpdate);
101
102 this.menu.constrainScroll = this.constrainScroll;
103 },
104
105 /**
106 * This overrides the constrainScroll method of Ext.menu.Menu. The only difference here is that the Y position
107 * and the height is NOT recalculated even if maxHeight is set.
108 *
109 * @param {Number} y
110 * @returns {Number}
111 */
112 constrainScroll: function(y) {
113 var max, full = this.ul.setHeight('auto').getHeight(),
114 returnY = y, normalY, parentEl, scrollTop, viewHeight;
115 if (this.floating){
116 parentEl = Ext.fly(this.el.dom.parentNode);
117 scrollTop = parentEl.getScroll().top;
118 viewHeight = parentEl.getViewSize().height;
119
120 normalY = y - scrollTop;
121 max = this.maxHeight ? this.maxHeight : viewHeight - normalY;
122 } else {
123 max = this.getHeight();
124 }
125
126 if (this.maxHeight){
127 max = Math.min(this.maxHeight, max);
128 }
129 if (full > max && max > 0){
130 this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0);
131 this.ul.setHeight(this.activeMax);
132 this.createScrollers();
133 this.el.select('.x-menu-scroller').setDisplayed('');
134 } else {
135 this.ul.setHeight(full);
136 this.el.select('.x-menu-scroller').setDisplayed('none');
137 }
138 this.ul.dom.scrollTop = 0;
139 return returnY;
140 },
141
142 /**
143 * Create the menu items config
144 */
145 buildItemsConfig: function () {
146 var itemsConfig = [];
147 // Walk through the editor toolbar configuration nested arrays: [ toolbar [ row [ group ] ] ]
148 var firstInGroup = true, convertedItemId;
149 var i, j ,k, n, m, p, row, group, itemId;
150 for (i = 0, n = this.editor.config.toolbar.length; i < n; i++) {
151 row = this.editor.config.toolbar[i];
152 // Add the groups
153 firstInGroup = true;
154 for (j = 0, m = row.length; j < m; j++) {
155 group = row[j];
156 if (!firstInGroup) {
157 // If a visible item was added to the line
158 itemsConfig.push({
159 xtype: 'menuseparator',
160 cls: 'separator'
161 });
162 }
163 firstInGroup = true;
164 // Add each item
165 for (k = 0, p = group.length; k < p; k++) {
166 itemId = group[k];
167 convertedItemId = this.editorConfiguration.convertButtonId[itemId];
168 if ((!this.showButtons || this.showButtons.indexOf(convertedItemId) !== -1)
169 && (!this.hideButtons || this.hideButtons.indexOf(convertedItemId) === -1)) {
170 var button = this.getButton(itemId);
171 // xtype is set through applied button configuration
172 if (button && button.xtype === 'htmlareabutton' && !button.hideInContextMenu) {
173 itemId = button.getItemId();
174 itemsConfig.push({
175 itemId: itemId,
176 cls: 'button',
177 overCls: 'hover',
178 text: (button.contextMenuTitle ? button.contextMenuTitle : button.tooltip),
179 iconCls: button.iconCls,
180 helpText: (button.helpText ? button.helpText : this.localize(itemId + '-tooltip')),
181 hidden: true
182 });
183 firstInGroup = false;
184 }
185 }
186 }
187 }
188 }
189 // If a visible item was added
190 if (!firstInGroup) {
191 itemsConfig.push({
192 xtype: 'menuseparator',
193 cls: 'separator'
194 });
195 }
196 // Add special target delete item
197 itemId = 'DeleteTarget';
198 itemsConfig.push({
199 itemId: itemId,
200 cls: 'button',
201 overCls: 'hover',
202 iconCls: 'htmlarea-action-delete-item',
203 helpText: this.localize('Remove this node from the document')
204 });
205 return itemsConfig;
206 },
207
208 /**
209 * Handler when the menu gets shown
210 */
211 onShow: function () {
212 var self = this;
213 Event.one(this.editor.document.documentElement, 'mousedown.contextmeu', function (event) { Event.stopEvent(event); self.menu.hide(); return false; });
214 },
215
216 /**
217 * Handler when the menu gets hidden
218 */
219 onHide: function () {
220 var self = this;
221 Event.off(this.editor.document.documentElement, 'mousedown.contextmeu');
222 },
223
224 /**
225 * Handler to show the context menu
226 */
227 show: function (event, target) {
228 Event.stopEvent(event);
229 // Need to wait a while for the toolbar state to be updated
230 var self = this;
231 window.setTimeout(function () {
232 self.showMenu(target);
233 }, 150);
234 return false;
235 },
236
237 /**
238 * Show the context menu
239 */
240 showMenu: function (target) {
241 this.showContextItems(target);
242 this.ranges = this.editor.getSelection().getRanges();
243 // Show the context menu
244 var iframePosition = Dom.getPosition(this.editor.iframe.getEl());
245 this.menu.showAt([
246 iframePosition.x + this.mousePosition.x,
247 document.body.scrollTop + iframePosition.y + this.mousePosition.y
248 ]);
249 },
250
251 /**
252 * Show items depending on context
253 */
254 showContextItems: function (target) {
255 var lastIsSeparator = false, lastIsButton = false, xtype, lastVisible;
256 this.menu.cascade(function (menuItem) {
257 xtype = menuItem.getXType();
258 if (xtype === 'menuseparator') {
259 menuItem.setVisible(lastIsButton);
260 lastIsButton = false;
261 } else if (xtype === 'menuitem') {
262 var button = this.getButton(menuItem.getItemId());
263 if (button) {
264 var text = button.contextMenuTitle ? button.contextMenuTitle : button.tooltip;
265 if (menuItem.text != text) {
266 menuItem.setText(text);
267 }
268 menuItem.helpText = button.helpText ? button.helpText : menuItem.helpText;
269 menuItem.setVisible(!button.disabled);
270 lastIsButton = lastIsButton || !button.disabled;
271 } else {
272 // Special target delete item
273 this.deleteTarget = target;
274 if (/^(html|body)$/i.test(target.nodeName)) {
275 this.deleteTarget = null;
276 } else if (/^(table|thead|tbody|tr|td|th|tfoot)$/i.test(target.nodeName)) {
277 this.deleteTarget = Dom.getFirstAncestorOfType(target, 'table');
278 } else if (/^(ul|ol|dl|li|dd|dt)$/i.test(target.nodeName)) {
279 this.deleteTarget = Dom.getFirstAncestorOfType(target, ['ul', 'ol', 'dl']);
280 }
281 if (this.deleteTarget) {
282 menuItem.setVisible(true);
283 menuItem.setText(this.localize('Remove the') + ' &lt;' + this.deleteTarget.nodeName.toLowerCase() + '&gt; ');
284 lastIsButton = true;
285 } else {
286 menuItem.setVisible(false);
287 }
288 }
289 }
290 if (!menuItem.hidden) {
291 lastVisible = menuItem;
292 }
293 }, this);
294 // Hide the last item if it is a separator
295 if (!lastIsButton) {
296 lastVisible.setVisible(false);
297 }
298 },
299
300 /**
301 * Handler invoked when a menu item is clicked on
302 */
303 onItemClick: function (item, event) {
304 this.editor.getSelection().setRanges(this.ranges);
305 var button = this.getButton(item.getItemId());
306 if (button) {
307 /**
308 * @event HTMLAreaEventContextMenu
309 * Fires when the button is triggered from the context menu
310 */
311 Event.trigger(button, 'HTMLAreaEventContextMenu', [button]);
312 } else if (item.getItemId() === 'DeleteTarget') {
313 // Do not leave a non-ie table cell empty
314 var parent = this.deleteTarget.parentNode;
315 parent.normalize();
316 if (!UserAgent.isIE && /^(td|th)$/i.test(parent.nodeName) && parent.childNodes.length == 1) {
317 // Do not leave a non-ie table cell empty
318 parent.appendChild(this.editor.document.createElement('br'));
319 }
320 // Try to find a reasonable replacement selection
321 var nextSibling = this.deleteTarget.nextSibling;
322 var previousSibling = this.deleteTarget.previousSibling;
323 if (nextSibling) {
324 this.editor.getSelection().selectNode(nextSibling, true);
325 } else if (previousSibling) {
326 this.editor.getSelection().selectNode(previousSibling, false);
327 }
328 Dom.removeFromParent(this.deleteTarget);
329 this.editor.updateToolbar();
330 }
331 },
332
333 /**
334 * Handler invoked when the editor is about to be destroyed
335 */
336 onBeforeDestroy: function (event) {
337 this.menu.removeAll(true);
338 this.menu.destroy();
339 }
340 });
341
342 return ContextMenu;
343
344 });