markdown-xblock 2.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markdown_xblock/__init__.py +2 -0
- markdown_xblock/html.py +335 -0
- markdown_xblock/public/plugins/codemirror/LICENSE +21 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/dialog/dialog.css +32 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/dialog/dialog.js +155 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/display/fullscreen.css +6 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/display/fullscreen.js +41 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/edit/matchbrackets.js +120 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/search/search.js +159 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/search/searchcursor.js +189 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/addon/selection/active-line.js +71 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/lib/codemirror.css +319 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/lib/codemirror.js +7923 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/css/css.js +717 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/htmlmixed/htmlmixed.js +121 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/javascript/javascript.js +684 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/markdown/markdown.js +883 -0
- markdown_xblock/public/plugins/codemirror/codemirror-4.8/mode/xml/xml.js +384 -0
- markdown_xblock/public/plugins/codemirror/langs/en.js +8 -0
- markdown_xblock/public/plugins/codemirror/plugin.min.js +1 -0
- markdown_xblock/public/plugins/codemirror/source.html +270 -0
- markdown_xblock/public/plugins/codesample/css/prism.css +141 -0
- markdown_xblock/public/plugins/codesample/js/prism.js +15 -0
- markdown_xblock/public/plugins/codesample/plugin.min.js +1 -0
- markdown_xblock/public/plugins/image/plugin.min.js +1 -0
- markdown_xblock/public/plugins/link/plugin.min.js +1 -0
- markdown_xblock/public/plugins/lists/plugin.min.js +1 -0
- markdown_xblock/public/plugins/textcolor/plugin.min.js +1 -0
- markdown_xblock/public/skin/content.inline.min.css +1 -0
- markdown_xblock/public/skin/content.min.css +2 -0
- markdown_xblock/public/skin/fonts/icomoon-small.eot +0 -0
- markdown_xblock/public/skin/fonts/icomoon-small.svg +175 -0
- markdown_xblock/public/skin/fonts/icomoon-small.ttf +0 -0
- markdown_xblock/public/skin/fonts/icomoon-small.woff +0 -0
- markdown_xblock/public/skin/fonts/icomoon.eot +0 -0
- markdown_xblock/public/skin/fonts/icomoon.svg +153 -0
- markdown_xblock/public/skin/fonts/icomoon.ttf +0 -0
- markdown_xblock/public/skin/fonts/icomoon.woff +0 -0
- markdown_xblock/public/skin/fonts/readme.md +1 -0
- markdown_xblock/public/skin/img/anchor.gif +0 -0
- markdown_xblock/public/skin/img/loader.gif +0 -0
- markdown_xblock/public/skin/img/object.gif +0 -0
- markdown_xblock/public/skin/img/trans.gif +0 -0
- markdown_xblock/public/skin/img/wline.gif +0 -0
- markdown_xblock/public/skin/skin.ie7.min.css +1 -0
- markdown_xblock/public/skin/skin.json +51 -0
- markdown_xblock/public/skin/skin.min.css +1 -0
- markdown_xblock/static/css/html.css +15 -0
- markdown_xblock/static/css/pygments.css +61 -0
- markdown_xblock/static/html/lms.html +1 -0
- markdown_xblock/static/html/studio.html +29 -0
- markdown_xblock/static/js/html.js +233 -0
- markdown_xblock/static/js/tinymce/themes/modern/theme.min.js +1 -0
- markdown_xblock/static/js/tinymce/tinymce.js +26801 -0
- markdown_xblock/static/js/tinymce/tinymce.min.js +2 -0
- markdown_xblock/translations/README.txt +4 -0
- markdown_xblock/utils.py +11 -0
- markdown_xblock-2.4.0.dist-info/METADATA +158 -0
- markdown_xblock-2.4.0.dist-info/RECORD +83 -0
- markdown_xblock-2.4.0.dist-info/WHEEL +5 -0
- markdown_xblock-2.4.0.dist-info/entry_points.txt +2 -0
- markdown_xblock-2.4.0.dist-info/licenses/LICENSE +671 -0
- markdown_xblock-2.4.0.dist-info/top_level.txt +5 -0
- releasenotes/notes/add-default-css-for-tables-3a53e02e8a350253.yaml +5 -0
- releasenotes/notes/add_public_view-d24763b7941dc2b0.yaml +4 -0
- releasenotes/notes/custom-css-classes-7984a9c95147d39c.yaml +5 -0
- releasenotes/notes/export-md-to-file-ab1528b4c28cb2f0.yaml +5 -0
- releasenotes/notes/fix-course-export-b35c78a22be0c6b5.yaml +6 -0
- releasenotes/notes/missing-codemirror-files-d64ccb15d0faf640.yaml +6 -0
- releasenotes/notes/pypi-release-3e4d5ceb7b6e252a.yaml +7 -0
- releasenotes/notes/python-3.12-support-dac839f8d01ac6b9.yaml +4 -0
- releasenotes/notes/refactor-ci-36abda8c15250d06.yaml +6 -0
- releasenotes/notes/refactor-export-import-49bbf0e8a364f139.yaml +8 -0
- releasenotes/notes/remove_py35-0b265861449b1753.yaml +9 -0
- releasenotes/notes/safe_mode_replace_escape-40fdb78ec8aa5eff.yaml +7 -0
- releasenotes/notes/show_in_read_only_mode-3af39459d5d56b33.yaml +7 -0
- requirements/base.txt +9 -0
- requirements/dev.txt +2 -0
- requirements/reno.txt +1 -0
- requirements/test.txt +9 -0
- tests/__init__.py +1 -0
- tests/test_basics.py +280 -0
- var/.placeholder +2 -0
markdown_xblock/html.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""This XBlock help creating a secure and easy-to-use HTML blocks in edx-platform."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import markdown2
|
|
7
|
+
from path import Path as path
|
|
8
|
+
from django.conf import settings as django_settings
|
|
9
|
+
from xblock.core import XBlock
|
|
10
|
+
from xblock.fields import List, Scope, String
|
|
11
|
+
from web_fragments.fragment import Fragment
|
|
12
|
+
from xblock.utils.resources import ResourceLoader
|
|
13
|
+
from xblock.utils.settings import XBlockWithSettingsMixin
|
|
14
|
+
from xblock.utils.studio_editable import StudioEditableXBlockMixin, loader
|
|
15
|
+
|
|
16
|
+
from .utils import _
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
19
|
+
xblock_loader = ResourceLoader(__name__) # pylint: disable=invalid-name
|
|
20
|
+
|
|
21
|
+
SETTINGS_KEY = 'markdown'
|
|
22
|
+
DEFAULT_EXTRAS = [
|
|
23
|
+
"code-friendly",
|
|
24
|
+
"fenced-code-blocks",
|
|
25
|
+
"footnotes",
|
|
26
|
+
"tables",
|
|
27
|
+
"use-file-vars"
|
|
28
|
+
]
|
|
29
|
+
DEFAULT_SETTINGS = {
|
|
30
|
+
"extras": DEFAULT_EXTRAS,
|
|
31
|
+
"safe_mode": 'replace'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_xblock_settings():
|
|
36
|
+
"""Extract xblock settings."""
|
|
37
|
+
try:
|
|
38
|
+
xblock_settings = django_settings.XBLOCK_SETTINGS
|
|
39
|
+
settings = xblock_settings.get(
|
|
40
|
+
SETTINGS_KEY, DEFAULT_SETTINGS)
|
|
41
|
+
except AttributeError:
|
|
42
|
+
settings = DEFAULT_SETTINGS
|
|
43
|
+
|
|
44
|
+
return settings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@XBlock.wants('settings')
|
|
48
|
+
class MarkdownXBlock(StudioEditableXBlockMixin, XBlockWithSettingsMixin, XBlock):
|
|
49
|
+
"""
|
|
50
|
+
This XBlock provides content editing in Markdown and displays it in HTML.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
display_name = String(
|
|
54
|
+
display_name=_('Display Name'),
|
|
55
|
+
help=_('The display name for this component.'),
|
|
56
|
+
scope=Scope.settings,
|
|
57
|
+
default=_('Markdown')
|
|
58
|
+
)
|
|
59
|
+
classes = List(
|
|
60
|
+
display_name=_('Classes'),
|
|
61
|
+
help=_('JSON list of strings representing custom CSS classes to add to this component'),
|
|
62
|
+
scope=Scope.settings,
|
|
63
|
+
default=[]
|
|
64
|
+
)
|
|
65
|
+
data = String(
|
|
66
|
+
help=_('The Markdown content for this module'),
|
|
67
|
+
default=u'',
|
|
68
|
+
scope=Scope.content
|
|
69
|
+
)
|
|
70
|
+
editor = 'markdown'
|
|
71
|
+
editable_fields = ('display_name', 'classes')
|
|
72
|
+
show_in_read_only_mode = True
|
|
73
|
+
|
|
74
|
+
@XBlock.supports('multi_device')
|
|
75
|
+
def student_view(self, context=None): # pylint: disable=unused-argument
|
|
76
|
+
"""
|
|
77
|
+
Return a fragment that contains the html for the student view.
|
|
78
|
+
"""
|
|
79
|
+
frag = Fragment()
|
|
80
|
+
frag.content = xblock_loader.render_django_template('static/html/lms.html', {'self': self})
|
|
81
|
+
|
|
82
|
+
frag.add_css_url(self.runtime.local_resource_url(self, 'public/plugins/codesample/css/prism.css'))
|
|
83
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, 'public/plugins/codesample/js/prism.js'))
|
|
84
|
+
|
|
85
|
+
frag.add_css_url(self.runtime.local_resource_url(self, 'static/css/pygments.css'))
|
|
86
|
+
frag.add_css_url(self.runtime.local_resource_url(self, 'static/css/html.css'))
|
|
87
|
+
|
|
88
|
+
return frag
|
|
89
|
+
|
|
90
|
+
def studio_view(self, context=None): # pylint: disable=unused-argument
|
|
91
|
+
"""
|
|
92
|
+
Return a fragment that contains the html for the Studio view.
|
|
93
|
+
"""
|
|
94
|
+
frag = Fragment()
|
|
95
|
+
settings_fields = self.get_editable_fields()
|
|
96
|
+
settings_page = loader.render_django_template('templates/studio_edit.html', {'fields': settings_fields})
|
|
97
|
+
context = {
|
|
98
|
+
'self': self,
|
|
99
|
+
'settings_page': settings_page,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
frag.content = xblock_loader.render_django_template('static/html/studio.html', context)
|
|
103
|
+
|
|
104
|
+
self.add_stylesheets(frag)
|
|
105
|
+
self.add_scripts(frag)
|
|
106
|
+
|
|
107
|
+
js_data = {
|
|
108
|
+
'editor': self.editor,
|
|
109
|
+
'skin_url': self.runtime.local_resource_url(self, 'public/skin'),
|
|
110
|
+
'external_plugins': self.get_editor_plugins()
|
|
111
|
+
}
|
|
112
|
+
frag.initialize_js('MarkdownXBlock', js_data)
|
|
113
|
+
|
|
114
|
+
return frag
|
|
115
|
+
|
|
116
|
+
@XBlock.supports("multi_device")
|
|
117
|
+
def public_view(self, context=None):
|
|
118
|
+
"""
|
|
119
|
+
Return the student_view when course is set to be public.
|
|
120
|
+
"""
|
|
121
|
+
return self.student_view(context)
|
|
122
|
+
|
|
123
|
+
@XBlock.json_handler
|
|
124
|
+
def update_content(self, data, suffix=''): # pylint: disable=unused-argument
|
|
125
|
+
"""
|
|
126
|
+
Update the saved HTML data with the new HTML passed in the JSON 'content' field.
|
|
127
|
+
"""
|
|
128
|
+
self.data = data['content']
|
|
129
|
+
|
|
130
|
+
return {'content': self.data}
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def workbench_scenarios():
|
|
134
|
+
"""A canned scenario for display in the workbench."""
|
|
135
|
+
return [
|
|
136
|
+
('MarkdownXBlock',
|
|
137
|
+
"""<vertical_demo>
|
|
138
|
+
<markdown>
|
|
139
|
+
# This is h1
|
|
140
|
+
## This is h2
|
|
141
|
+
```
|
|
142
|
+
This is a code block
|
|
143
|
+
```
|
|
144
|
+
* This is
|
|
145
|
+
* an unordered
|
|
146
|
+
* list
|
|
147
|
+
This is a regular paragraph
|
|
148
|
+
1. This is
|
|
149
|
+
1. an ordered
|
|
150
|
+
1. list
|
|
151
|
+
*This is italic*
|
|
152
|
+
**This is bold**
|
|
153
|
+
</markdown>
|
|
154
|
+
</vertical_demo>
|
|
155
|
+
"""),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def add_stylesheets(self, frag):
|
|
159
|
+
"""
|
|
160
|
+
A helper method to add all necessary styles to the fragment.
|
|
161
|
+
:param frag: The fragment that will hold the scripts.
|
|
162
|
+
"""
|
|
163
|
+
frag.add_css_url(self.runtime.local_resource_url(self, 'static/css/html.css'))
|
|
164
|
+
|
|
165
|
+
frag.add_css_url(self.runtime.local_resource_url(
|
|
166
|
+
self, 'public/plugins/codemirror/codemirror-4.8/lib/codemirror.css'))
|
|
167
|
+
|
|
168
|
+
def add_scripts(self, frag):
|
|
169
|
+
"""
|
|
170
|
+
A helper method to add all necessary scripts to the fragment.
|
|
171
|
+
:param frag: The fragment that will hold the scripts.
|
|
172
|
+
"""
|
|
173
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, 'static/js/tinymce/tinymce.min.js'))
|
|
174
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, 'static/js/tinymce/themes/modern/theme.min.js'))
|
|
175
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, 'static/js/html.js'))
|
|
176
|
+
frag.add_javascript(loader.load_unicode('public/studio_edit.js'))
|
|
177
|
+
|
|
178
|
+
code_mirror_dir = 'public/plugins/codemirror/codemirror-4.8/'
|
|
179
|
+
|
|
180
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, code_mirror_dir + 'lib/codemirror.js'))
|
|
181
|
+
frag.add_javascript_url(self.runtime.local_resource_url(self, code_mirror_dir + 'mode/markdown/markdown.js'))
|
|
182
|
+
|
|
183
|
+
def get_editor_plugins(self):
|
|
184
|
+
"""
|
|
185
|
+
This method will generate a list of external plugins urls to be used in TinyMCE editor.
|
|
186
|
+
These plugins should live in `public` directory for us to generate URLs for.
|
|
187
|
+
|
|
188
|
+
const PLUGINS_DIR = "/resource/html5/public/plugins/";
|
|
189
|
+
const EXTERNAL_PLUGINS = PLUGINS.map(function(p) { return PLUGINS_DIR + p + "/plugin.min.js" });
|
|
190
|
+
|
|
191
|
+
:return: A list of URLs
|
|
192
|
+
"""
|
|
193
|
+
plugin_path = 'public/plugins/{plugin}/plugin.min.js'
|
|
194
|
+
plugins = ['codesample', 'image', 'link', 'lists', 'textcolor', 'codemirror']
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
plugin: self.runtime.local_resource_url(self, plugin_path.format(plugin=plugin)) for plugin in plugins
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
def substitute_keywords(self, html):
|
|
201
|
+
"""
|
|
202
|
+
Replaces all %%-encoded words using KEYWORD_FUNCTION_MAP mapping functions.
|
|
203
|
+
|
|
204
|
+
Iterates through all keywords that must be substituted and replaces them by calling the corresponding functions
|
|
205
|
+
stored in `keywords`. If the function throws a specified exception, the substitution is not performed.
|
|
206
|
+
|
|
207
|
+
Functions stored in `keywords` must either:
|
|
208
|
+
- return a replacement string
|
|
209
|
+
- throw `KeyError` or `AttributeError`, `TypeError`.
|
|
210
|
+
"""
|
|
211
|
+
data = html
|
|
212
|
+
system = getattr(self, 'system', None)
|
|
213
|
+
if not system: # This shouldn't happen, but if `system` is missing, then skip substituting keywords.
|
|
214
|
+
return data
|
|
215
|
+
|
|
216
|
+
keywords = {
|
|
217
|
+
'%%USER_ID%%': lambda: getattr(system, 'anonymous_student_id'),
|
|
218
|
+
'%%COURSE_ID%%': lambda: getattr(system, 'course_id').html_id(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for key, substitutor in keywords.items():
|
|
222
|
+
if key in data:
|
|
223
|
+
try:
|
|
224
|
+
data = data.replace(key, substitutor())
|
|
225
|
+
except (KeyError, AttributeError, TypeError):
|
|
226
|
+
# Do not replace the keyword when substitutor is not present.
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
return data
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def html(self):
|
|
233
|
+
"""
|
|
234
|
+
A property that returns the markdown content data as html.
|
|
235
|
+
"""
|
|
236
|
+
settings = get_xblock_settings()
|
|
237
|
+
extras = settings.get("extras",
|
|
238
|
+
DEFAULT_SETTINGS['extras'])
|
|
239
|
+
safe_mode = settings.get("safe_mode",
|
|
240
|
+
DEFAULT_SETTINGS['safe_mode'])
|
|
241
|
+
|
|
242
|
+
html = markdown2.markdown(
|
|
243
|
+
self.data,
|
|
244
|
+
extras=extras,
|
|
245
|
+
safe_mode=safe_mode
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
html = self.substitute_keywords(html)
|
|
249
|
+
|
|
250
|
+
return html
|
|
251
|
+
|
|
252
|
+
def get_editable_fields(self):
|
|
253
|
+
"""
|
|
254
|
+
This method extracts the editable fields from this XBlock and returns them after validating them.
|
|
255
|
+
|
|
256
|
+
Part of this method's copied from StudioEditableXBlockMixin#submit_studio_edits
|
|
257
|
+
with some modifications..
|
|
258
|
+
:return: A list of the editable fields with the information that
|
|
259
|
+
the template needs to render a form field for them.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
fields = []
|
|
263
|
+
|
|
264
|
+
# Build a list of all the fields that can be edited:
|
|
265
|
+
for field_name in self.editable_fields:
|
|
266
|
+
field = self.fields[field_name] # pylint: disable=unsubscriptable-object
|
|
267
|
+
assert field.scope in (Scope.content, Scope.settings), (
|
|
268
|
+
'Only Scope.content or Scope.settings fields can be used with '
|
|
269
|
+
'StudioEditableXBlockMixin. Other scopes are for user-specific data and are '
|
|
270
|
+
'not generally created/configured by content authors in Studio.'
|
|
271
|
+
)
|
|
272
|
+
field_info = self._make_field_info(field_name, field)
|
|
273
|
+
if field_info is not None:
|
|
274
|
+
fields.append(field_info)
|
|
275
|
+
|
|
276
|
+
return fields
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def parse_xml(cls, node, runtime, keys):
|
|
280
|
+
"""
|
|
281
|
+
Use `node` to construct a new block.
|
|
282
|
+
"""
|
|
283
|
+
block = runtime.construct_xblock_from_class(cls, keys)
|
|
284
|
+
id_generator = runtime.id_generator
|
|
285
|
+
|
|
286
|
+
# Read markdown content from file and add to editor.
|
|
287
|
+
url_name = node.get('url_name', node.get('slug'))
|
|
288
|
+
location = id_generator.create_definition(node.tag, url_name)
|
|
289
|
+
|
|
290
|
+
filename = node.get('filename')
|
|
291
|
+
pointer_path = "{category}/{url_path}".format(
|
|
292
|
+
category='markdown',
|
|
293
|
+
url_path=location.block_id.replace(':', '/')
|
|
294
|
+
)
|
|
295
|
+
base = path(pointer_path).dirname()
|
|
296
|
+
filepath = u"{base}/{name}.md".format(base=base, name=filename)
|
|
297
|
+
|
|
298
|
+
with runtime.resources_fs.open(filepath, encoding='utf-8') as infile:
|
|
299
|
+
markdown = infile.read()
|
|
300
|
+
block.data = markdown
|
|
301
|
+
|
|
302
|
+
# Attributes become fields.
|
|
303
|
+
for name, value in list(node.items()): # lxml has no iteritems
|
|
304
|
+
cls._set_field_if_present(block, name, value, {})
|
|
305
|
+
|
|
306
|
+
return block
|
|
307
|
+
|
|
308
|
+
def add_xml_to_node(self, node):
|
|
309
|
+
"""
|
|
310
|
+
For exporting, set data on etree.Element `node`.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
# Write markdown data to file
|
|
314
|
+
pathname = self.url_name.replace(':', '/')
|
|
315
|
+
filepath = u'{category}/{pathname}.md'.format(
|
|
316
|
+
category=self.category,
|
|
317
|
+
pathname=pathname
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
self.runtime.export_fs.makedirs(
|
|
321
|
+
os.path.dirname(filepath),
|
|
322
|
+
recreate=True)
|
|
323
|
+
|
|
324
|
+
with self.runtime.export_fs.open(filepath, 'wb') as filestream:
|
|
325
|
+
markdown_data = self.data.encode('utf-8')
|
|
326
|
+
filestream.write(markdown_data)
|
|
327
|
+
|
|
328
|
+
# Write out the markdown file name
|
|
329
|
+
filename = path(pathname).basename()
|
|
330
|
+
|
|
331
|
+
node.tag = self.category
|
|
332
|
+
node.set("filename", filename)
|
|
333
|
+
node.set('xblock-family', self.entry_point)
|
|
334
|
+
node.set('display_name', self.display_name)
|
|
335
|
+
node.set('classes', str(self.classes))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.CodeMirror-dialog {
|
|
2
|
+
position: absolute;
|
|
3
|
+
left: 0; right: 0;
|
|
4
|
+
background: white;
|
|
5
|
+
z-index: 15;
|
|
6
|
+
padding: .1em .8em;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
color: #333;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.CodeMirror-dialog-top {
|
|
12
|
+
border-bottom: 1px solid #eee;
|
|
13
|
+
top: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.CodeMirror-dialog-bottom {
|
|
17
|
+
border-top: 1px solid #eee;
|
|
18
|
+
bottom: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.CodeMirror-dialog input {
|
|
22
|
+
border: none;
|
|
23
|
+
outline: none;
|
|
24
|
+
background: transparent;
|
|
25
|
+
width: 20em;
|
|
26
|
+
color: inherit;
|
|
27
|
+
font-family: monospace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.CodeMirror-dialog button {
|
|
31
|
+
font-size: 70%;
|
|
32
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
2
|
+
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
3
|
+
|
|
4
|
+
// Open simple dialogs on top of an editor. Relies on dialog.css.
|
|
5
|
+
|
|
6
|
+
(function(mod) {
|
|
7
|
+
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
8
|
+
mod(require("../../lib/codemirror"));
|
|
9
|
+
else if (typeof define == "function" && define.amd) // AMD
|
|
10
|
+
define(["../../lib/codemirror"], mod);
|
|
11
|
+
else // Plain browser env
|
|
12
|
+
mod(CodeMirror);
|
|
13
|
+
})(function(CodeMirror) {
|
|
14
|
+
function dialogDiv(cm, template, bottom) {
|
|
15
|
+
var wrap = cm.getWrapperElement();
|
|
16
|
+
var dialog;
|
|
17
|
+
dialog = wrap.appendChild(document.createElement("div"));
|
|
18
|
+
if (bottom)
|
|
19
|
+
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
|
|
20
|
+
else
|
|
21
|
+
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
|
|
22
|
+
|
|
23
|
+
if (typeof template == "string") {
|
|
24
|
+
dialog.innerHTML = template;
|
|
25
|
+
} else { // Assuming it's a detached DOM element.
|
|
26
|
+
dialog.appendChild(template);
|
|
27
|
+
}
|
|
28
|
+
return dialog;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function closeNotification(cm, newVal) {
|
|
32
|
+
if (cm.state.currentNotificationClose)
|
|
33
|
+
cm.state.currentNotificationClose();
|
|
34
|
+
cm.state.currentNotificationClose = newVal;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
|
|
38
|
+
if (!options) options = {};
|
|
39
|
+
|
|
40
|
+
closeNotification(this, null);
|
|
41
|
+
|
|
42
|
+
var dialog = dialogDiv(this, template, options.bottom);
|
|
43
|
+
var closed = false, me = this;
|
|
44
|
+
function close(newVal) {
|
|
45
|
+
if (typeof newVal == 'string') {
|
|
46
|
+
inp.value = newVal;
|
|
47
|
+
} else {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
dialog.parentNode.removeChild(dialog);
|
|
51
|
+
me.focus();
|
|
52
|
+
|
|
53
|
+
if (options.onClose) options.onClose(dialog);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var inp = dialog.getElementsByTagName("input")[0], button;
|
|
58
|
+
if (inp) {
|
|
59
|
+
if (options.value) {
|
|
60
|
+
inp.value = options.value;
|
|
61
|
+
inp.select();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.onInput)
|
|
65
|
+
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
|
|
66
|
+
if (options.onKeyUp)
|
|
67
|
+
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
|
|
68
|
+
|
|
69
|
+
CodeMirror.on(inp, "keydown", function(e) {
|
|
70
|
+
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
|
|
71
|
+
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
|
|
72
|
+
inp.blur();
|
|
73
|
+
CodeMirror.e_stop(e);
|
|
74
|
+
close();
|
|
75
|
+
}
|
|
76
|
+
if (e.keyCode == 13) callback(inp.value);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
|
80
|
+
|
|
81
|
+
inp.focus();
|
|
82
|
+
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
|
83
|
+
CodeMirror.on(button, "click", function() {
|
|
84
|
+
close();
|
|
85
|
+
me.focus();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
|
|
89
|
+
|
|
90
|
+
button.focus();
|
|
91
|
+
}
|
|
92
|
+
return close;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
|
|
96
|
+
closeNotification(this, null);
|
|
97
|
+
var dialog = dialogDiv(this, template, options && options.bottom);
|
|
98
|
+
var buttons = dialog.getElementsByTagName("button");
|
|
99
|
+
var closed = false, me = this, blurring = 1;
|
|
100
|
+
function close() {
|
|
101
|
+
if (closed) return;
|
|
102
|
+
closed = true;
|
|
103
|
+
dialog.parentNode.removeChild(dialog);
|
|
104
|
+
me.focus();
|
|
105
|
+
}
|
|
106
|
+
buttons[0].focus();
|
|
107
|
+
for (var i = 0; i < buttons.length; ++i) {
|
|
108
|
+
var b = buttons[i];
|
|
109
|
+
(function(callback) {
|
|
110
|
+
CodeMirror.on(b, "click", function(e) {
|
|
111
|
+
CodeMirror.e_preventDefault(e);
|
|
112
|
+
close();
|
|
113
|
+
if (callback) callback(me);
|
|
114
|
+
});
|
|
115
|
+
})(callbacks[i]);
|
|
116
|
+
CodeMirror.on(b, "blur", function() {
|
|
117
|
+
--blurring;
|
|
118
|
+
setTimeout(function() { if (blurring <= 0) close(); }, 200);
|
|
119
|
+
});
|
|
120
|
+
CodeMirror.on(b, "focus", function() { ++blurring; });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* openNotification
|
|
126
|
+
* Opens a notification, that can be closed with an optional timer
|
|
127
|
+
* (default 5000ms timer) and always closes on click.
|
|
128
|
+
*
|
|
129
|
+
* If a notification is opened while another is opened, it will close the
|
|
130
|
+
* currently opened one and open the new one immediately.
|
|
131
|
+
*/
|
|
132
|
+
CodeMirror.defineExtension("openNotification", function(template, options) {
|
|
133
|
+
closeNotification(this, close);
|
|
134
|
+
var dialog = dialogDiv(this, template, options && options.bottom);
|
|
135
|
+
var closed = false, doneTimer;
|
|
136
|
+
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
|
|
137
|
+
|
|
138
|
+
function close() {
|
|
139
|
+
if (closed) return;
|
|
140
|
+
closed = true;
|
|
141
|
+
clearTimeout(doneTimer);
|
|
142
|
+
dialog.parentNode.removeChild(dialog);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
CodeMirror.on(dialog, 'click', function(e) {
|
|
146
|
+
CodeMirror.e_preventDefault(e);
|
|
147
|
+
close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (duration)
|
|
151
|
+
doneTimer = setTimeout(close, duration);
|
|
152
|
+
|
|
153
|
+
return close;
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
2
|
+
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
3
|
+
|
|
4
|
+
(function(mod) {
|
|
5
|
+
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
6
|
+
mod(require("../../lib/codemirror"));
|
|
7
|
+
else if (typeof define == "function" && define.amd) // AMD
|
|
8
|
+
define(["../../lib/codemirror"], mod);
|
|
9
|
+
else // Plain browser env
|
|
10
|
+
mod(CodeMirror);
|
|
11
|
+
})(function(CodeMirror) {
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
CodeMirror.defineOption("fullScreen", false, function(cm, val, old) {
|
|
15
|
+
if (old == CodeMirror.Init) old = false;
|
|
16
|
+
if (!old == !val) return;
|
|
17
|
+
if (val) setFullscreen(cm);
|
|
18
|
+
else setNormal(cm);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function setFullscreen(cm) {
|
|
22
|
+
var wrap = cm.getWrapperElement();
|
|
23
|
+
cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,
|
|
24
|
+
width: wrap.style.width, height: wrap.style.height};
|
|
25
|
+
wrap.style.width = "";
|
|
26
|
+
wrap.style.height = "auto";
|
|
27
|
+
wrap.className += " CodeMirror-fullscreen";
|
|
28
|
+
document.documentElement.style.overflow = "hidden";
|
|
29
|
+
cm.refresh();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setNormal(cm) {
|
|
33
|
+
var wrap = cm.getWrapperElement();
|
|
34
|
+
wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, "");
|
|
35
|
+
document.documentElement.style.overflow = "";
|
|
36
|
+
var info = cm.state.fullScreenRestore;
|
|
37
|
+
wrap.style.width = info.width; wrap.style.height = info.height;
|
|
38
|
+
window.scrollTo(info.scrollLeft, info.scrollTop);
|
|
39
|
+
cm.refresh();
|
|
40
|
+
}
|
|
41
|
+
});
|