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