Jupyter: Move cell to top of notebook - javascript

Often I realize halfway through a notebook that I forgot an import and I want to move it to the top of the notebook (where I try to keep most of my imports). Is there a way to add a keyboard shortcut to ~/.jupyter/custom/custom.js that moves a cell to the top of a notebook?
Currently I do this by cutting the cell, scrolling to the top of the notebook, pasting and scrolling back down to where I was (often losing my place on the way back).
Below is some code from fastai forums to accomplish a different task: going to the running cell:
Jupyter.keyboard_manager.command_shortcuts.add_shortcut('CMD-I', {
help : 'Go to Running cell',
help_index : 'zz',
handler : function (event) {
setTimeout(function() {
// Find running cell and click the first one
if ($('.running').length > 0) {
//alert("found running cell");
$('.running')[0].scrollIntoView();
}}, 250);
return false;
}
});

It's partially documented; however you have to know a little JavaScript to create a keyboard shortcut.
As you've figured out, edit custom.js to bind a keyboard shortcut, as mentioned in the documentation (the second method must be used if the action is not built-in)
For the documentation, read the source code (either online or in site-packages/notebook/static/notebook/js/*.js if you installed jupyter locally). In particular, actions.js contains example bindings and notebook.js contains the functions that you can call on the notebook.
This is an example. It has the disadvantage of modifying the jupyter clipboard. To avoid that, you might be able to look into the source code of notebook.move_cell_down function to see how it's implemented, and modify it correspondingly.
Jupyter.keyboard_manager.command_shortcuts.add_shortcut('cmdtrl-I', {
help : 'Move cell to first in notebook',
handler : function (env) {
var index = env.notebook.get_selected_index();
env.notebook.cut_cell();
env.notebook.select(0);
env.notebook.paste_cell_above();
env.notebook.select(index);
}
});

Related

How to detect when a menu is rendered in tinymce 4?

In tinymce 4, the menu bar is rendered but each menu is rendered only on click.
To illustrate this, notice that each menu from the menu bar has the mce-menu class.
At any time, if no menu is open, if you try to get the set of menus, you'll fail because they aren't rendered yet:
var menuSet = $('.mce-menu');
// menuSet.length : 0
But if you click a menu bar header, let say the insert menu, it will be rendered and opened. Now, keeping it open, going to the console and retrying:
var menuSet = $('.mce-menu');
// menuSet.length : 1
and you'll get the opened menu.
Now if you close it clicking anywhere out of the opened menu, and retrying:
var menuSet = $('.mce-menu');
// menuSet.length : 1
... the menu is not removed from the DOM. It's a good news: since the menu was rendered once, we can get and manipulate it.
I have some DOM manipulation to do with each .mce-menu elements, but I'll have to now when each menu is opened the first time.
But how to handle a such event ?
I can't get any clue from the official documentation nor the forums, or anywhere.
It's definitely possible, but we both were not aware enough of how JS events are managed.
I tried to code my events handlers the old way :
$('body').on('click', function() {
do_stuff();
});
While I had to do it the new, correct way :
$('body').on('click', '.mce-btn', function() {
do_stuff();
});
With this, the events are correctly managed.
Try to use onPostrender funtion :
editor.addMenuItem("mybtn", {
type: "menuitem",
name: 'mybtn',
onPostRender:function (){
// write your code here//
},
I resolved the issue by writting a convenient `Tinymce 4 plugin focused on that purpose.
Of course this plugin is open-sourced by the GNU GPL v2 license, following the original Tinymce licensing policy.
Tinymce Plugin MenusController:
https://github.com/sirap-group/tinymce-plugin-menuscontroller
But I didn't wrote the documentation yet, my apologies.
However, here is how you can use it:
Install the plugin
Download the latest release tarball from github or, even better, install it from bower:
bower install tinymce-plugin-menuscontroller
If you don't know bower, discover it here: https://bower.io (npm i -g bower; bower --help).
The npm package isn't available yet, I'd provide it soon (but any Pull Request on github is welcome...).
By default, the plugin folder would be downoaded and placed in ./bower_components. If you've installed tinymce the same way, you've got also ./bower_components/tinymce or ./bower_components/tinymce-dist.
You don't need to add the script to your index.html file because tinymce load it itself if you setup it correctly.
So you need to :
symlink it to the tinymce plugin folder:
$ cd ./bower_components/tinymce/plugins
$ ln -s ../../tinymce-plugin-menuscontroller menuscontroller
load it in tinymce init. For example:
tinymce.init({
selector: 'textarea',
// [...]
plugins: 'menuscontroller'
})
Get the plugin instance:
var editor = window.tinymce.activeEditor
var menusCtl = editor.plugins.menuscontroller
// at this point, if menusCtl is undefined, something gone wrong in the setup step: please check the previous steps.
Plugin API (v0.2.1)
Instance Methods
Get the menu bar:
menusCtl.getMenubar()
Get each menu by the name it was registered with:
menusCtl.getMenuByName(String: name)
Get the toolbars
menusCtl.getToolbars
Events
Event: menusController:mceMenuRendered event
When any tinymce menu is rendered
$('body').on('menusController:mceMenuRendered', function (evt, menuDomNode) {
console.log(menuDomNode)
})
The menusController:mceMenuRendered event is called one for each menu of the active editor menubar, when it is rendered, so when the user click the dropdown menu (File link for the "file" menu, Insert for the "insert" menu, etc...).
Event: menusController:mceMenuItemRendered:<menuDomID>
When any menu item is rendered. Let's say we've created a menu item with the my-custom-menu-item identifier. So tinymce set its DOM ID to my-custom-menu-item. Thus, the MenusController plugin will create and bind to body the following event:
menusController:mceMenuItemRendered:my-custom-menu-item
So you can handle the rendered event of your custom menu item listening on it:
$('body').on('menusController:mceMenuItemRendered:my-custom-menu-item',
function (evt, menuItemDomNode) {
console.log(menuItemDomNode)
}
)
MenusController API (v0.3.0+)
A the time of wrinting (Monday, mars the 13th, 2017), the last released version is the v0.2.1. But the v0.3.0 is planned to be released soon, and will provide a new event, more useful than the last.
Event: menusController:mceMenuItemRendered
When you need to know the menu item ID to handle the event menusController:mceMenuItemRendered:<menuDomID> and get the menu item DOM Node as callback argument, the event menusController:mceMenuItemRendered don't needs it but provides it as callback argument for each new rendered menu item:
$('body').on('menusController:mceMenuItemRendered',
function (evt, menuItemID) {
console.log(menuItemID) // 'my-custom-menu-item'
// So you can hanlde all menu item even if you don't know its ID
// And you can also handle the DOM Node with the selector by ID
var selector = '#' + menuItemID
var menuItem = $(selector)
console.log(menuItem) // jQuery object (the menu item)
}
)
With tinymce you can fully customize the menu buttons via the editor object:
tinymce.init({
/*....*/
setup: function(editor) {
editor.addButton('mybutton', {
type: 'menubutton',
text: 'My button',
icon: false,
onclick: function(){
alert('Some Message');
},
menu: [{
text: 'Menu item 1',
onclick: function() {
alert('Some Message');
}
}]
});
}
/*....*/
});
Unfortunately you cannot insert html inside the text property, but i think you can do that with more research. You can also create a callback function for the click event on the menu button.
Personally, I will use tinymce official api to modify the dom instead of doing some other event driven dom manipulation.
You can find more a good example here

Can't find element using UI hash in Marionette Layout

I'm not sure why I can't get the button element using my UI hash. This is what my Layout looks like:
Layout: App.Base.Objects.BaseLayout.extend({
// Rest of the code left out for brevity
ui: {
btnSave: "#btnSave"
},
events: {
"click #ui.btnSave": "onSave"
},
onInitialize: function () {
this.listenTo(App.vent, "DisableSaveButton", function(val) {
this.disableSaveButton(val);
},this);
},
disableSaveButton: function () {
this.ui.btnSave.prop("disabled",val).toggleClass("ui-state-disabled",val);
},
onSave: function () {
alert("saved!");
}
})
In VS2013, when my breakpoint hits the line inside disableSaveButton method, I entered $("#btnSave") into the Watch window and I was able to get the element back. I could tell because it had a length of 1. From this, I know the button is rendered. However, if I enter this.ui.btnSave into the Watch window, I would get an element with length of 0.
My BaseLayout object is basically a custom object extended from Marionette.Layout
Marionette version: 1.8.8
Any ideas why I can't find the button element using this.ui.btnSave?
Thanks in advance!
Got some help from a coworker and the issue might be because the element is out of scope. Basically, inside the Layout object, 'this' does not contain the element. We were able replace 'this.ui.btnSave' with '$("#btnSave",this.buttonset.el)' and that works fine. buttonset is the region that actually contains the html element.
This seems like an inconsistency because even though the ui hash didn't work, the click event utilizing the ui hash did work.
UPDATE 6/3/2015:
Another coworker of mine provided a better solution. Basically, in my Layout I use a display function to display my view. It looks something like this:
Layout: App.Base.Objects.BaseLayout.extend({
// Rest of the code left out for brevity
display: function() {
$(this.buttonset.el).html(_.template($("#buttonset-view").html(), {"viewType": viewType}));
}
})
Basically, I'm saying to set the html of my region, which is this.buttonset.el, to my template's html. As of now, my layout doesn't know any of the elements inside the region. It just contains a region which displays the elements. So there is some sort of disconnect between my layout and the elements in my region.
The correct solution, as opposed to my earlier workaround, is to simply add the following line of code at the end:
this.bindUIElements();
From Marionette Annotated Source:
This method binds the elements specified in the “ui” hash inside the
view’s code with the associated jQuery selectors.
So this final code looks like this:
Layout: App.Base.Objects.BaseLayout.extend({
// Rest of the code left out for brevity
display: function() {
$(this.buttonset.el).html(_.template($("#buttonset-view").html(), {"viewType": viewType}));
this.bindUIElements();
}
})
With this, I was able to finally able to retrieve my element using this.ui.btnSave.

CKEditor Change Config Setting on Focus / Click

I need to be able to change the filebrowserUploadUrl of CKEditor when I change some details on the page, as the querystring I pass through is used by the custom upload process I've put in place.
I'm using the JQuery plugin. Here's my code:
$('#Content').ckeditor({
extraPlugins: 'autogrow',
autoGrow_maxHeight: 400,
removePlugins: 'resize'
});
$("#Content").ckeditorGet().on("instanceReady", function () {
this.on("focus", function () {
// Define browser Url from selected fields
this.config.filebrowserUploadUrl = filebrowserUploadUrl: '/my-path-to-upload-script/?ID1=' + $("ID1").val() + '&ID2=' + $("#ID2").val();
});
});
This works fine the first time, but if I come out of the dialogue and change the value of #ID1 and #ID2, it keeps the previous values. When I debug, the filebrowserUploadUrl is set correctly, but it doesn't affect the submission values. It seems the config values are cached.
Is there any way to change a config value on the fly?
Currently I don't see any possibility to change this URL on the fly without hacking.
Take a look at http://dev.ckeditor.com/browser/CKEditor/trunk/_source/plugins/filebrowser/plugin.js#L306
This element.filebrowser.url property is set once and as you can see few lines above it will be reused again. You can try to somehow find this element and reset this property, but not having deeper understanding of the code of this plugin I don't know how.
Second option would be to change this line #L284 to:
url = undefined;
However, I haven't check if this is the correct solution :) Good luck!
BTW. Feel free to fill an issue on http://dev.ckeditor.com.
I solved this by reloading the editor whenever a change occurred; I actually went through the source code for the browser plugin etc, but couldn't get any changes to work (and of course, I really didn't want to change anything for future upgrades).
function setFileBrowserUrl() {
// Remove editor instance
$("#Content").ckeditorGet().destroy();
// Recreate editor instance (needed to reset the file browser url)
createEditor();
}
function createEditor() {
$('#Content').ckeditor({
filebrowserUploadUrl: '/my-path-to-upload-script/?ID1=' + $("ID1").val() + '&ID2=' + $("#ID2").val(),
extraPlugins: 'autogrow',
autoGrow_maxHeight: 400,
removePlugins: 'resize'
});
}
Then I call setFileBrowserUrl every time the relevant elements on the page change. Not ideal, but it works for my purposes :)

grumble.js, jQuery plugin (Bubble popups): how to make it not polute my document body with unremovable trash?

I want to show a popup many on click. I want that many to be in a bubble. So I created a demo: here. But that Bubble generator plugin i use tends to keep tons of trash in the DOM each time it shows a popup. Well so I tried to destroy trash via
$('.grumble-text').remove();
$('.grumble').remove();
$('.grumble-button').remove();
But it somehow brakes it at all=( So how to change grumble-bubble popup plugin code to make it either keep DOM clean or at least make plugin independent of trash it creates?
I've recently updated the plugin to provide better control of positioning and angle. The update also persists the grumble, invoking the plugin more than once on an element will not create extra left over DOM.
Try updating to the latest code. The code below should now work as you expect.
var html = ''
+'Download me'
+'<br/>'
+'Edit me'
+'<br/>'
+'Delete me';
var $grumble = $('#grumble3');
$grumble.mouseup(function(eventObj) {
$grumble.grumble({
text: html ,
angle: (Math.random() * 360 + 150),
distance: 30,
hideOnClick: true,
onShow: function() {
$grumble.addClass("hilight");
},
onBeginHide: function() {
$grumble.removeClass("hilight");
}
});
}).mousedown(function() {
$grumble.addClass("hilight");
});
Thanks for your interest. If there are any further problems please raise them as bugs on the github page. https://github.com/jamescryer/grumble.js
Use the grumble and button parameters on the onHide callback like this:
$('#grumble').grumble({
text: 'Whoaaa, this is a lot of text that i couldn\'t predict',
angle: 85,
distance: 50,
showAfter: 4000,
hideAfter: 2000,
onHide: function(grumble, button) {
grumble.bubble.remove();
grumble.text.remove();
button && button.remove();
}
});
This allows you to remove only the "trash" (I prefer "leftovers") associated with that specific tooltip/popup/bubble. Note that button only exists if hasHideButton is true, hence the button && existence check.
Why do you want to remove it? Is the 'trash' causing problems with browser performance?
In general, the only way to do this is to dig into the plugin source and add a function to remove the plugin, if one is not already present. If you just remove the related DOM elements you will leave behind references to them and events handlers that access them.

ExtJS: starting HtmlEditor defaulting to source

I'm using ExtJS 3.2.1 and I need a component almost identical to the bundled HtmlEditor, with one exception: it must start editing the HTML source code directly. The reason I don't use a normal TextArea is that the user should be able to preview the result of his actions before submitting.
I've tried calling toggleSourceEdit(), as per ExtJS documentation, with no success. Debugging, I see that the editor object has the sourceEditMode property set to true, and the Source Edit button seems as if it was "pressed", but clicking on it does not render the typed HTML, and clicking it again goes to the Source Mode.
I've tried calling toggleSourceEdit() after the container show() method, on the container afterLayout listener and on the editor afterRender listener. I've tried also calling it on another button that I added to the container. The result is the same on every try.
The only other option I see is updating ExtJS to 3.3.0, but I haven't seem anything related on the changelogs. Either way, it's going to be my next step. EDIT: The app had another problems when updating, we'll make a bigger effort to update later. As of right now, we are using the HtmlEditor in its original setting.
Thanks!
ran into the same problem (using 3.3.0 by the way)
stumbled upon a fix by dumb luck. i have no idea why this works, but second time is the charm. call it twice in a row to achieve the desired effect..
HTMLEditor.toggleSourceEdit(true);
HTMLEditor.toggleSourceEdit(true);
hope that helps!
Rather calling toggleSourceEdit(), try to setup the configuration while you create HtmlEditor Object
Using toggleSourceEdit() caused some problems for me. One was that this seemed to put the editor somewhere in limbo between source edit and WYSIWYG mode unless I used a timeout of 250ms or so. It also puts the focus in that editor, and I don't want to start the form's focus in the editor, especially since it's below the fold and the browser scrolls to the focused html editor when it opens.
The only thing that worked for me was to extend Ext.form.HtmlEditor and then overwrite toggleSourceEdit, removing the focus command. Then adding a listener for toggling to the source editor when the component is initialized. This is for Ext 4.1 and up. For older versions, replace me.updateLayout() with me.doComponentLayout().
var Namespace = {
SourceEditor: Ext.define('Namespace.SourceEditor', {
extend: 'Ext.form.HtmlEditor',
alias: 'widget.sourceeditor',
initComponent: function() {
this.callParent(arguments);
},
toggleSourceEdit: function (sourceEditMode) {
var me = this,
iframe = me.iframeEl,
textarea = me.textareaEl,
hiddenCls = Ext.baseCSSPrefix + 'hidden',
btn = me.getToolbar().getComponent('sourceedit');
if (!Ext.isBoolean(sourceEditMode)) {
sourceEditMode = !me.sourceEditMode;
}
me.sourceEditMode = sourceEditMode;
if (btn.pressed !== sourceEditMode) {
btn.toggle(sourceEditMode);
}
if (sourceEditMode) {
me.disableItems(true);
me.syncValue();
iframe.addCls(hiddenCls);
textarea.removeCls(hiddenCls);
textarea.dom.removeAttribute('tabindex');
//textarea.focus();
me.inputEl = textarea;
} else {
if (me.initialized) {
me.disableItems(me.readOnly);
}
me.pushValue();
iframe.removeCls(hiddenCls);
textarea.addCls(hiddenCls);
textarea.dom.setAttribute('tabindex', -1);
me.deferFocus();
me.inputEl = iframe;
}
me.fireEvent('editmodechange', me, sourceEditMode);
me.updateLayout();
}
})
}
Then to use it:
Ext.create('Namespace.SourceEditor', {
/*regular options*/
listeners: {
initialize: function(thisEditor) {
thisEditor.toggleSourceEdit();
}
}
});
htmlEditor.toggleSourceEdit(true);
one time should be enough if you do this listening to the afterrender event of the editor.

Categories

Resources