Skip to content

Commit

Permalink
Add quill.js based TextEditor widget (#2875)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Nov 3, 2021
1 parent afc8d1e commit d13e5f6
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 6 deletions.
181 changes: 181 additions & 0 deletions examples/reference/widgets/TextEditor.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import param\n",
"\n",
"pn.extension('texteditor')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``TextEditor`` widget allows provides a WYSIWYG (what-you-see-is-what-you-get) rich text editor into a Panel application which outputs HTML. The editor is built on top of the [Quill.js](https://quilljs.com/) library.\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"* **``disabled``** (boolean): Whether the editor is disabled\n",
"* **``mode``** (str): Whether to display a `'toolbar'` or a `'bubble'` menu on highlight.\n",
"* **``placeholder``** (str): Placeholder output to render when the editor is empty.\n",
"* **``toolbar``** (boolean or list): Toolbar configuration either as a boolean toggle or a configuration specified as a list.\n",
"* **``value``** (str): The current HTML output of the widget\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To construct a `TextEditor` editor widget we must declare it explicitly and may provide a `placeholder` as pure text or a `value` as HTML:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"wysiwyg = pn.widgets.TextEditor(placeholder='Enter some text')\n",
"wysiwyg"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The current state of the editor output is reflected on the `value` parameter:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"wysiwyg.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `value` may also be set:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"wysiwyg.value = '<h1>A title</h1>'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Toolbar\n",
"\n",
"The toolbar of the editor can be configured in a variety of ways, the simplest of which is simply toggling it on and off by setting `toolbar=True/False`. Beyond that we can provide the formatting options to display in a number of configurations which are explained in the [quill.js documentation](https://quilljs.com/docs/modules/toolbar/#container). The examples below"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.config.sizing_mode = 'stretch_width'\n",
"\n",
"# Flat list of options\n",
"flat = pn.widgets.TextEditor(toolbar=['bold', 'italic', 'underline'])\n",
"\n",
"# Grouped options\n",
"grouped = pn.widgets.TextEditor(toolbar=[['bold', 'italic'], ['link', 'image']])\n",
"\n",
"# Dropdown of options\n",
"dropdown = pn.widgets.TextEditor(toolbar=[{'size': [ 'small', False, 'large', 'huge' ]}])\n",
"\n",
"# Full configuration\n",
"full_config = pn.widgets.TextEditor(toolbar=[\n",
" ['bold', 'italic', 'underline', 'strike'], # toggled buttons\n",
" ['blockquote', 'code-block'],\n",
"\n",
" [{ 'header': 1 }, { 'header': 2 }], # custom button values\n",
" [{ 'list': 'ordered'}, { 'list': 'bullet' }],\n",
" [{ 'script': 'sub'}, { 'script': 'super' }], # superscript/subscript\n",
" [{ 'indent': '-1'}, { 'indent': '+1' }], # outdent/indent\n",
" [{ 'direction': 'rtl' }], # text direction\n",
"\n",
" [{ 'size': ['small', False, 'large', 'huge'] }], # custom dropdown\n",
" [{ 'header': [1, 2, 3, 4, 5, 6, False] }],\n",
"\n",
" [{ 'color': [] }, { 'background': [] }], # dropdown with defaults from theme\n",
" [{ 'font': [] }],\n",
" [{ 'align': [] }],\n",
"\n",
" ['clean'] # remove formatting button\n",
"])\n",
"\n",
"pn.Column(\n",
" pn.Row(flat, grouped, dropdown),\n",
" full_config\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Instead of a toolbar we can also switch to `'bubble'` mode which displays a hover menu when highlighting the text:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.widgets.TextEditor(mode='bubble', value='Highlight me to edit formatting', margin=(40, 0, 0, 0), height=200, width=400)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Controls\n",
"\n",
"The `TextEditor` widget exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"editor = pn.widgets.TextEditor(placeholder='Enter some text')\n",
"\n",
"pn.Row(editor.controls(jslink=True, sizing_mode='fixed'), editor)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
19 changes: 16 additions & 3 deletions panel/_templates/autoload_panel_js.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ calls it with the rendered model.
return null;
}
console.debug("Bokeh: BokehJS not loaded, scheduling load and callback at", now());
root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length;

function on_load() {
root._bokeh_is_loading--;
Expand Down Expand Up @@ -93,8 +92,12 @@ calls it with the rendered model.
{% if r in exports %}
window.{{ exports[r] }} = {{ exports[r] }}
{% endif %}
on_load()
})
{% endfor %}
root._bokeh_is_loading = css_urls.length + {{ requirements|length }};
} else {
root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length;
}
{%- for lib, urls in skip_imports.items() %}
if (((window['{{ lib }}'] !== undefined) && (!(window['{{ lib }}'] instanceof HTMLElement))) || window.requirejs) {
Expand All @@ -106,7 +109,12 @@ calls it with the rendered model.
{%- endfor %}
for (var i = 0; i < js_urls.length; i++) {
var url = js_urls[i];
if (skip.indexOf(url) >= 0) { on_load(); continue; }
if (skip.indexOf(url) >= 0) {
if (!window.requirejs) {
on_load();
}
continue;
}
var element = document.createElement('script');
element.onload = on_load;
element.onerror = on_error;
Expand All @@ -117,7 +125,12 @@ calls it with the rendered model.
}
for (var i = 0; i < js_modules.length; i++) {
var url = js_modules[i];
if (skip.indexOf(url) >= 0) { on_load(); continue; }
if (skip.indexOf(url) >= 0) {
if (!window.requirejs) {
on_load();
}
continue;
}
var element = document.createElement('script');
element.onload = on_load;
element.onerror = on_error;
Expand Down
3 changes: 2 additions & 1 deletion panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ class panel_extension(_pyviz_extension):
'perspective': 'panel.models.perspective',
'terminal': 'panel.models.terminal',
'tabulator': 'panel.models.tabulator',
'gridstack': 'panel.layout.gridstack'
'gridstack': 'panel.layout.gridstack',
'texteditor': 'panel.models.quill'
}

# Check whether these are loaded before rendering (if any item
Expand Down
4 changes: 4 additions & 0 deletions panel/dist/css/widgets.css
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,7 @@ progress:not([value])::before {
.bk-root .json-formatter-row .json-formatter-string, .bk-root .json-formatter-row .json-formatter-stringifiable {
white-space: pre-wrap;
}

.ql-bubble .ql-editor {
border: 1px solid #ccc;
}
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {Perspective} from "./perspective"
export {Player} from "./player"
export {PlotlyPlot} from "./plotly"
export {Progress} from "./progress"
export {QuillInput} from "./quill"
export {ReactiveHTML} from "./reactive_html"
export {SingleSelect} from "./singleselect"
export {SpeechToText} from "./speech_to_text"
Expand Down
51 changes: 51 additions & 0 deletions panel/models/quill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from bokeh.core.properties import Any, Bool, Enum, Either, List, String
from bokeh.models import HTMLBox

from ..io.resources import bundled_files
from ..util import classproperty


class QuillInput(HTMLBox):
"""
WYSIWYG text editor based on Quill.js
"""

__css_raw__ = [
'https://cdn.quilljs.com/1.3.6/quill.bubble.css',
'https://cdn.quilljs.com/1.3.6/quill.snow.css'
]

__javascript_raw__ = [
'https://cdn.quilljs.com/1.3.6/quill.js',
]

@classproperty
def __javascript__(cls):
return bundled_files(cls)

@classproperty
def __css__(cls):
return bundled_files(cls, 'css')

@classproperty
def __js_skip__(cls):
return {'Quill': cls.__javascript__}

__js_require__ = {
'paths': {
'Quill': 'https://cdn.quilljs.com/1.3.6/quill',
},
'exports': {
'Quill': 'Quill'
}
}

mode = Enum("bubble", "toolbar", default='toolbar')

placeholder = String()

readonly = Bool(False)

text = String()

toolbar = Either(List(Any), Bool)
Loading

0 comments on commit d13e5f6

Please sign in to comment.