From 2f9c7fd8393088735b91d060f6082f44b271ab06 Mon Sep 17 00:00:00 2001 From: Joscha Legewie Date: Mon, 3 Feb 2014 14:03:36 +0100 Subject: [PATCH 1/2] TinyMCE Improvements: Open links from note window with popup and auto-create links --- .../zotero/bindings/styled-textbox.xml | 4 + resource/tinymce/note.html | 4 +- .../tinymce/plugins/autolink/editor_plugin.js | 1 + .../plugins/contextmenu/editor_plugin.js | 166 +++++++++++++++++- .../plugins/linksmenu/editor_plugin.js | 156 ++++++++++++++++ 5 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 resource/tinymce/plugins/autolink/editor_plugin.js create mode 100644 resource/tinymce/plugins/linksmenu/editor_plugin.js diff --git a/chrome/content/zotero/bindings/styled-textbox.xml b/chrome/content/zotero/bindings/styled-textbox.xml index 4dcd2a3ad..b17587a92 100644 --- a/chrome/content/zotero/bindings/styled-textbox.xml +++ b/chrome/content/zotero/bindings/styled-textbox.xml @@ -138,6 +138,10 @@ case 'change': break; + + case 'openlink': + ZoteroPane.loadURI(event.target.href); + break; default: return; diff --git a/resource/tinymce/note.html b/resource/tinymce/note.html index 86e4dda18..0b10dc1a5 100644 --- a/resource/tinymce/note.html +++ b/resource/tinymce/note.html @@ -50,10 +50,10 @@ fix_list_elements : true, fix_table_elements : true, - plugins : "paste,contextmenu,directionality", + plugins : "paste,contextmenu,linksmenu,directionality,autolink", // Theme options - theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,blockquote,|,link,unlink,|,%DIR%", + theme_advanced_buttons1 : "bold,italic,underline,strikethrough,|,sub,sup,|,forecolor,backcolor,|,blockquote,|,link,|,%DIR%", theme_advanced_buttons2 : "formatselect,|,justifyleft,justifycenter,justifyright,|,bullist,numlist,outdent,indent,|,removeformat,code", theme_advanced_buttons3 : "", theme_advanced_toolbar_location : "top", diff --git a/resource/tinymce/plugins/autolink/editor_plugin.js b/resource/tinymce/plugins/autolink/editor_plugin.js new file mode 100644 index 000000000..71d86bbec --- /dev/null +++ b/resource/tinymce/plugins/autolink/editor_plugin.js @@ -0,0 +1 @@ +(function(){tinymce.create("tinymce.plugins.AutolinkPlugin",{init:function(a,b){var c=this;a.onKeyDown.addToTop(function(d,f){if(f.keyCode==13){return c.handleEnter(d)}});if(tinyMCE.isIE){return}a.onKeyPress.add(function(d,f){if(f.which==41){return c.handleEclipse(d)}});a.onKeyUp.add(function(d,f){if(f.keyCode==32){return c.handleSpacebar(d)}})},handleEclipse:function(a){this.parseCurrentLine(a,-1,"(",true)},handleSpacebar:function(a){this.parseCurrentLine(a,0,"",true)},handleEnter:function(a){this.parseCurrentLine(a,-1,"",false)},parseCurrentLine:function(i,d,b,g){var a,f,c,n,k,m,h,e,j;a=i.selection.getRng(true).cloneRange();if(a.startOffset<5){e=a.endContainer.previousSibling;if(e==null){if(a.endContainer.firstChild==null||a.endContainer.firstChild.nextSibling==null){return}e=a.endContainer.firstChild.nextSibling}j=e.length;a.setStart(e,j);a.setEnd(e,j);if(a.endOffset<5){return}f=a.endOffset;n=e}else{n=a.endContainer;if(n.nodeType!=3&&n.firstChild){while(n.nodeType!=3&&n.firstChild){n=n.firstChild}if(n.nodeType==3){a.setStart(n,0);a.setEnd(n,n.nodeValue.length)}}if(a.endOffset==1){f=2}else{f=a.endOffset-1-d}}c=f;do{a.setStart(n,f>=2?f-2:0);a.setEnd(n,f>=1?f-1:0);f-=1}while(a.toString()!=" "&&a.toString()!=""&&a.toString().charCodeAt(0)!=160&&(f-2)>=0&&a.toString()!=b);if(a.toString()==b||a.toString().charCodeAt(0)==160){a.setStart(n,f);a.setEnd(n,c);f+=1}else{if(a.startOffset==0){a.setStart(n,0);a.setEnd(n,c)}else{a.setStart(n,f);a.setEnd(n,c)}}var m=a.toString();if(m.charAt(m.length-1)=="."){a.setEnd(n,c-1)}m=a.toString();h=m.match(/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+-]+@)(.+)$/i);if(h){if(h[1]=="www."){h[1]="http://www."}else{if(/@$/.test(h[1])&&!/^mailto:/.test(h[1])){h[1]="mailto:"+h[1]}}k=i.selection.getBookmark();i.selection.setRng(a);tinyMCE.execCommand("createlink",false,h[1]+h[2]);i.selection.moveToBookmark(k);i.nodeChanged();if(tinyMCE.isWebKit){i.selection.collapse(false);var l=Math.min(n.length,c+1);a.setStart(n,l);a.setEnd(n,l);i.selection.setRng(a)}}},getInfo:function(){return{longname:"Autolink",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autolink",version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add("autolink",tinymce.plugins.AutolinkPlugin)})(); \ No newline at end of file diff --git a/resource/tinymce/plugins/contextmenu/editor_plugin.js b/resource/tinymce/plugins/contextmenu/editor_plugin.js index 2ed042c3a..d420d5a15 100644 --- a/resource/tinymce/plugins/contextmenu/editor_plugin.js +++ b/resource/tinymce/plugins/contextmenu/editor_plugin.js @@ -1 +1,165 @@ -(function(){var a=tinymce.dom.Event,c=tinymce.each,b=tinymce.DOM;tinymce.create("tinymce.plugins.ContextMenu",{init:function(f){var i=this,g,d,j,e;i.editor=f;d=f.settings.contextmenu_never_use_native;i.onContextMenu=new tinymce.util.Dispatcher(this);e=function(k){h(f,k)};g=f.onContextMenu.add(function(k,l){if((j!==0?j:l.ctrlKey)&&!d){return}a.cancel(l);if(l.target.nodeName=="IMG"){k.selection.select(l.target)}i._getMenu(k).showMenu(l.clientX||l.pageX,l.clientY||l.pageY);a.add(k.getDoc(),"click",e);k.nodeChanged()});f.onRemove.add(function(){if(i._menu){i._menu.removeAll()}});function h(k,l){j=0;if(l&&l.button==2){j=l.ctrlKey;return}if(i._menu){i._menu.removeAll();i._menu.destroy();a.remove(k.getDoc(),"click",e);i._menu=null}}f.onMouseDown.add(h);f.onKeyDown.add(h);f.onKeyDown.add(function(k,l){if(l.shiftKey&&!l.ctrlKey&&!l.altKey&&l.keyCode===121){a.cancel(l);g(k,l)}})},getInfo:function(){return{longname:"Contextmenu",author:"Moxiecode Systems AB",authorurl:"http://tinymce.moxiecode.com",infourl:"http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/contextmenu",version:tinymce.majorVersion+"."+tinymce.minorVersion}},_getMenu:function(e){var g=this,d=g._menu,j=e.selection,f=j.isCollapsed(),h=j.getNode()||e.getBody(),i,k;if(d){d.removeAll();d.destroy()}k=b.getPos(e.getContentAreaContainer());d=e.controlManager.createDropMenu("contextmenu",{offset_x:k.x+e.getParam("contextmenu_offset_x",0),offset_y:k.y+e.getParam("contextmenu_offset_y",0),constrain:1,keyboard_focus:true});g._menu=d;d.add({title:"advanced.cut_desc",icon:"cut",cmd:"Cut"}).setDisabled(f);d.add({title:"advanced.copy_desc",icon:"copy",cmd:"Copy"}).setDisabled(f);d.add({title:"advanced.paste_desc",icon:"paste",cmd:"Paste"});if((h.nodeName=="A"&&!e.dom.getAttrib(h,"name"))||!f){d.addSeparator();d.add({title:"advanced.link_desc",icon:"link",cmd:e.plugins.advlink?"mceAdvLink":"mceLink",ui:true});d.add({title:"advanced.unlink_desc",icon:"unlink",cmd:"UnLink"})}d.addSeparator();d.add({title:"advanced.image_desc",icon:"image",cmd:e.plugins.advimage?"mceAdvImage":"mceImage",ui:true});d.addSeparator();i=d.addMenu({title:"contextmenu.align"});i.add({title:"contextmenu.left",icon:"justifyleft",cmd:"JustifyLeft"});i.add({title:"contextmenu.center",icon:"justifycenter",cmd:"JustifyCenter"});i.add({title:"contextmenu.right",icon:"justifyright",cmd:"JustifyRight"});i.add({title:"contextmenu.full",icon:"justifyfull",cmd:"JustifyFull"});g.onContextMenu.dispatch(g,d,h,f);return d}});tinymce.PluginManager.add("contextmenu",tinymce.plugins.ContextMenu)})(); \ No newline at end of file +/** + * editor_plugin_src.js + * + * Copyright 2009, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://tinymce.moxiecode.com/license + * Contributing: http://tinymce.moxiecode.com/contributing + */ + +(function() { + var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM; + + /** + * This plugin a context menu to TinyMCE editor instances. + * + * @class tinymce.plugins.ContextMenu + */ + tinymce.create('tinymce.plugins.ContextMenu', { + /** + * Initializes the plugin, this will be executed after the plugin has been created. + * This call is done before the editor instance has finished it's initialization so use the onInit event + * of the editor instance to intercept that event. + * + * @method init + * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. + * @param {string} url Absolute URL to where the plugin is located. + */ + init : function(ed) { + var t = this, showMenu, contextmenuNeverUseNative, realCtrlKey, hideMenu; + + t.editor = ed; + + contextmenuNeverUseNative = ed.settings.contextmenu_never_use_native; + + /** + * This event gets fired when the context menu is shown. + * + * @event onContextMenu + * @param {tinymce.plugins.ContextMenu} sender Plugin instance sending the event. + * @param {tinymce.ui.DropMenu} menu Drop down menu to fill with more items if needed. + */ + t.onContextMenu = new tinymce.util.Dispatcher(this); + + hideMenu = function(e) { + hide(ed, e); + }; + + showMenu = ed.onContextMenu.add(function(ed, e) { + // Block TinyMCE menu on ctrlKey and work around Safari issue + if ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative) + return; + + Event.cancel(e); + + // Select the image if it's clicked. WebKit would other wise expand the selection + if (e.target.nodeName == 'IMG') + ed.selection.select(e.target); + + t._getMenu(ed).showMenu(e.clientX || e.pageX, e.clientY || e.pageY); + Event.add(ed.getDoc(), 'click', hideMenu); + + ed.nodeChanged(); + }); + + ed.onRemove.add(function() { + if (t._menu) + t._menu.removeAll(); + }); + + function hide(ed, e) { + realCtrlKey = 0; + + // Since the contextmenu event moves + // the selection we need to store it away + if (e && e.button == 2) { + realCtrlKey = e.ctrlKey; + return; + } + + if (t._menu) { + t._menu.removeAll(); + t._menu.destroy(); + Event.remove(ed.getDoc(), 'click', hideMenu); + t._menu = null; + } + }; + + ed.onMouseDown.add(hide); + ed.onKeyDown.add(hide); + ed.onKeyDown.add(function(ed, e) { + if (e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 121) { + Event.cancel(e); + showMenu(ed, e); + } + }); + }, + + /** + * Returns information about the plugin as a name/value array. + * The current keys are longname, author, authorurl, infourl and version. + * + * @method getInfo + * @return {Object} Name/value array containing information about the plugin. + */ + getInfo : function() { + return { + longname : 'Contextmenu', + author : 'Moxiecode Systems AB', + authorurl : 'http://tinymce.moxiecode.com', + infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/contextmenu', + version : tinymce.majorVersion + "." + tinymce.minorVersion + }; + }, + + _getMenu : function(ed) { + var t = this, m = t._menu, se = ed.selection, col = se.isCollapsed(), el = se.getNode() || ed.getBody(), am, p; + + if (m) { + m.removeAll(); + m.destroy(); + } + + p = DOM.getPos(ed.getContentAreaContainer()); + + m = ed.controlManager.createDropMenu('contextmenu', { + offset_x : p.x + ed.getParam('contextmenu_offset_x', 0), + offset_y : p.y + ed.getParam('contextmenu_offset_y', 0), + constrain : 1, + keyboard_focus: true + }); + + t._menu = m; + + m.add({title : 'advanced.cut_desc', icon : 'cut', cmd : 'Cut'}).setDisabled(col); + m.add({title : 'advanced.copy_desc', icon : 'copy', cmd : 'Copy'}).setDisabled(col); + m.add({title : 'advanced.paste_desc', icon : 'paste', cmd : 'Paste'}); + + if ((el.nodeName == 'A' && !ed.dom.getAttrib(el, 'name')) || !col) { + m.addSeparator(); + // Added by Zotero + if(el.nodeName == 'A') m.add({title : 'Open Link', icon : 'link', cmd : 'openlink', ui : true }); + m.add({title : 'advanced.link_desc', icon : 'link', cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', ui : true}); + m.add({title : 'advanced.unlink_desc', icon : 'unlink', cmd : 'UnLink'}); + } + + m.addSeparator(); + m.add({title : 'advanced.image_desc', icon : 'image', cmd : ed.plugins.advimage ? 'mceAdvImage' : 'mceImage', ui : true}); + + m.addSeparator(); + am = m.addMenu({title : 'contextmenu.align'}); + am.add({title : 'contextmenu.left', icon : 'justifyleft', cmd : 'JustifyLeft'}); + am.add({title : 'contextmenu.center', icon : 'justifycenter', cmd : 'JustifyCenter'}); + am.add({title : 'contextmenu.right', icon : 'justifyright', cmd : 'JustifyRight'}); + am.add({title : 'contextmenu.full', icon : 'justifyfull', cmd : 'JustifyFull'}); + + t.onContextMenu.dispatch(t, m, el, col); + + return m; + } + }); + + // Register plugin + tinymce.PluginManager.add('contextmenu', tinymce.plugins.ContextMenu); +})(); \ No newline at end of file diff --git a/resource/tinymce/plugins/linksmenu/editor_plugin.js b/resource/tinymce/plugins/linksmenu/editor_plugin.js new file mode 100644 index 000000000..eaf9c21e6 --- /dev/null +++ b/resource/tinymce/plugins/linksmenu/editor_plugin.js @@ -0,0 +1,156 @@ +(function() { + var Event = tinymce.dom.Event, each = tinymce.each, DOM = tinymce.DOM; + + /** + * This plugin adds a left-click context menu to links in the TinyMCE editor for Zotero. + * Code adopted and modified from TinyMCE contextmenu plugin. + * + * @class tinymce.plugins.LinksMenu + */ + tinymce.create('tinymce.plugins.LinksMenu', { + /** + * Initializes the plugin, this will be executed after the plugin has been created. + * This call is done before the editor instance has finished it's initialization so use the onInit event + * of the editor instance to intercept that event. + * + * @method init + * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. + * @param {string} url Absolute URL to where the plugin is located. + */ + init : function(ed) { + var t = this, showMenu, contextmenuNeverUseNative, realCtrlKey, hideMenu; + + t.editor = ed; + + contextmenuNeverUseNative = ed.settings.contextmenu_never_use_native; + + // add editor command to open links through zoteroHandleEvent + ed.addCommand('openlink', function(command) { + var node = tinyMCE.activeEditor.selection.getNode(); + if (node.nodeName == 'A') { + zoteroHandleEvent({ type: 'openlink', target: node }); + } + }); + + /** + * This event gets fired when the context menu is shown. + * + * @event onClick + * @param {tinymce.plugins.LinksMenu} sender Plugin instance sending the event. + * @param {tinymce.ui.DropMenu} menu Drop down menu to fill with more items if needed. + */ + t.onClick = new tinymce.util.Dispatcher(this); + + hideMenu = function(e) { + hide(ed, e); + }; + + showMenu = ed.onClick.add(function(ed, e) { + // only show when node + Block TinyMCE menu on ctrlKey and work around Safari issue + if (e.target.nodeName != 'A' || ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative)) + return; + + Event.cancel(e); + + t._getMenu(ed).showMenu(e.clientX || e.pageX, e.clientY || e.pageY); + Event.add(ed.getDoc(), 'click', hideMenu); + + ed.nodeChanged(); + }); + + ed.onRemove.add(function() { + if (t._menu) + t._menu.removeAll(); + }); + + function hide(ed, e) { + realCtrlKey = 0; + + // Since the contextmenu event moves + // the selection we need to store it away + if (e && e.button == 2) { + realCtrlKey = e.ctrlKey; + return; + } + + if (t._menu) { + t._menu.removeAll(); + t._menu.destroy(); + Event.remove(ed.getDoc(), 'click', hideMenu); + t._menu = null; + } + }; + + ed.onMouseDown.add(hide); + ed.onKeyDown.add(hide); + ed.onKeyDown.add(function(ed, e) { + if (e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 121) { + Event.cancel(e); + showMenu(ed, e); + } + }); + }, + + /** + * Returns information about the plugin as a name/value array. + * The current keys are longname, author, authorurl, infourl and version. + * + * @method getInfo + * @return {Object} Name/value array containing information about the plugin. + */ + getInfo : function() { + return { + longname : 'Linksmenu', + author : '', + authorurl : '', + infourl : '', + version : tinymce.majorVersion + "." + tinymce.minorVersion + }; + }, + + _getMenu : function(ed) { + var t = this, m = t._menu, se = ed.selection, col = se.isCollapsed(), el = se.getNode() || ed.getBody(), am, p; + + if (m) { + m.removeAll(); + m.destroy(); + } + + p = DOM.getPos(ed.getContentAreaContainer()); + + m = ed.controlManager.createDropMenu('linksmenu', { + offset_x : p.x + ed.getParam('contextmenu_offset_x', 0), + offset_y : p.y + ed.getParam('contextmenu_offset_y', 0), + constrain : 1, + keyboard_focus: true + }); + + t._menu = m; + + m.add({ + title : 'Open Link', + icon : 'link', + cmd : 'openlink', + ui : true + }); + m.add({ + title : 'Edit Link', + icon : 'link', + cmd : ed.plugins.advlink ? 'mceAdvLink' : 'mceLink', + ui : true + }); + m.add({ + title : 'advanced.unlink_desc', + icon : 'unlink', + cmd : 'UnLink' + }); + + t.onClick.dispatch(t, m, el, col); + + return m; + } + }); + + // Register plugin + tinymce.PluginManager.add('linksmenu', tinymce.plugins.LinksMenu); +})(); From a6ab904cd0d2fa06c408450f541618a3c953d8f2 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 4 Feb 2014 20:55:12 -0500 Subject: [PATCH 2/2] Note link tweaks - Send modifier keys through to loadURI() when clicking Open Link in notes - Open link in parent window from external note window - Don't show both menus on right-click Follow-up from #450 --- .../zotero/bindings/styled-textbox.xml | 5 +++- .../plugins/linksmenu/editor_plugin.js | 28 ++++++++++++++++--- resource/tinymce/tiny_mce.js | 10 +++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/chrome/content/zotero/bindings/styled-textbox.xml b/chrome/content/zotero/bindings/styled-textbox.xml index b17587a92..443532728 100644 --- a/chrome/content/zotero/bindings/styled-textbox.xml +++ b/chrome/content/zotero/bindings/styled-textbox.xml @@ -140,7 +140,10 @@ break; case 'openlink': - ZoteroPane.loadURI(event.target.href); + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + win = wm.getMostRecentWindow('navigator:browser'); + win.ZoteroPane.loadURI(event.target.href, event.modifierKeys); break; default: diff --git a/resource/tinymce/plugins/linksmenu/editor_plugin.js b/resource/tinymce/plugins/linksmenu/editor_plugin.js index eaf9c21e6..77c6d223f 100644 --- a/resource/tinymce/plugins/linksmenu/editor_plugin.js +++ b/resource/tinymce/plugins/linksmenu/editor_plugin.js @@ -26,9 +26,18 @@ // add editor command to open links through zoteroHandleEvent ed.addCommand('openlink', function(command) { - var node = tinyMCE.activeEditor.selection.getNode(); + var ed = tinyMCE.activeEditor; + var node = ed.selection.getNode(); if (node.nodeName == 'A') { - zoteroHandleEvent({ type: 'openlink', target: node }); + zoteroHandleEvent({ + type: 'openlink', + target: node, + // We don't seem to be able to access the click event that triggered this + // command in order to check the modifier keys used, so instead we save + // the keys on every menu click in tiny_mce.js and pass them on here + // for use by loadURI(). + modifierKeys: ed.lastClickModifierKeys + }); } }); @@ -46,9 +55,20 @@ }; showMenu = ed.onClick.add(function(ed, e) { - // only show when node + Block TinyMCE menu on ctrlKey and work around Safari issue - if (e.target.nodeName != 'A' || ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative)) + // Only show on left-click + if (e.button != 0) { return; + } + + // Only show when node + if (e.target.nodeName != 'A') { + return; + } + + // Block TinyMCE menu on ctrlKey and work around Safari issue + if ((realCtrlKey !== 0 ? realCtrlKey : e.ctrlKey) && !contextmenuNeverUseNative) { + return; + } Event.cancel(e); diff --git a/resource/tinymce/tiny_mce.js b/resource/tinymce/tiny_mce.js index 847975edb..adc7bead1 100644 --- a/resource/tinymce/tiny_mce.js +++ b/resource/tinymce/tiny_mce.js @@ -11646,6 +11646,16 @@ tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { t.mouseClickFunc = Event.add(co, 'click', function(e) { var m; + // Added by Zotero + // + // Record the modifier keys used with the last menu click -- used by linksmenu plugin + tinymce.activeEditor.lastClickModifierKeys = { + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + shiftKey: e.shiftKey + }; + e = e.target; if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) {