aboutsummaryrefslogtreecommitdiff
path: root/sources/samples/toolbarconfigurator
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2016-01-25 17:45:33 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2016-01-25 18:00:33 +0100
commit7adcb81e4f83f98c468889aaa5a85558ba88c770 (patch)
tree0d6ede733777b29060b48df4afaa2c64bfbae276 /sources/samples/toolbarconfigurator
downloadconnexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.gz
connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.zst
connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.zip
Initial commit4.5.6
Diffstat (limited to 'sources/samples/toolbarconfigurator')
-rw-r--r--sources/samples/toolbarconfigurator/bender.js52
-rw-r--r--sources/samples/toolbarconfigurator/css/fontello.css55
-rw-r--r--sources/samples/toolbarconfigurator/font/LICENSE.txt12
-rw-r--r--sources/samples/toolbarconfigurator/font/config.json28
-rw-r--r--sources/samples/toolbarconfigurator/font/fontello.eotbin0 -> 4988 bytes
-rw-r--r--sources/samples/toolbarconfigurator/font/fontello.svg14
-rw-r--r--sources/samples/toolbarconfigurator/font/fontello.ttfbin0 -> 4820 bytes
-rw-r--r--sources/samples/toolbarconfigurator/font/fontello.woffbin0 -> 2904 bytes
-rw-r--r--sources/samples/toolbarconfigurator/index.html446
-rw-r--r--sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js566
-rw-r--r--sources/samples/toolbarconfigurator/js/fulltoolbareditor.js365
-rw-r--r--sources/samples/toolbarconfigurator/js/toolbarmodifier.js1366
-rw-r--r--sources/samples/toolbarconfigurator/js/toolbartextmodifier.js623
-rw-r--r--sources/samples/toolbarconfigurator/less/base.less38
-rw-r--r--sources/samples/toolbarconfigurator/less/toolbarmodifier.less508
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/LICENSE19
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/README.md12
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/codemirror.css325
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/codemirror.js8738
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/javascript.js701
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/neo.css36
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/show-hint.css38
-rw-r--r--sources/samples/toolbarconfigurator/lib/codemirror/show-hint.js392
-rw-r--r--sources/samples/toolbarconfigurator/package.json12
-rw-r--r--sources/samples/toolbarconfigurator/tests/one.js9
25 files changed, 14355 insertions, 0 deletions
diff --git a/sources/samples/toolbarconfigurator/bender.js b/sources/samples/toolbarconfigurator/bender.js
new file mode 100644
index 00000000..d592866c
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/bender.js
@@ -0,0 +1,52 @@
1/* jshint browser: false, node: true */
2
3'use strict';
4
5var config = {
6
7 applications: {
8 ckeditor: {
9 path: '../../',
10 files: [
11 'ckeditor.js'
12 ]
13 },
14
15 codemirror: {
16 path: '.',
17 files: [
18 'js/lib/codemirror/codemirror.js'
19 ]
20 },
21
22 toolbartool: {
23 path: '.',
24 files: [
25 'js/fulltoolbareditor.js',
26 'js/abstracttoolbarmodifier.js',
27 'js/toolbarmodifier.js',
28 'js/toolbartextmodifier.js'
29 ]
30 }
31 },
32
33 plugins: [
34 'node_modules/benderjs-mocha',
35 'node_modules/benderjs-chai'
36 ],
37
38 framework: 'mocha',
39
40 tests: {
41 'main': {
42 applications: [ 'ckeditor', 'codemirror', 'toolbartool' ],
43 basePath: 'tests/',
44 paths: [
45 '**',
46 '!**/_*/**'
47 ]
48 }
49 }
50};
51
52module.exports = config;
diff --git a/sources/samples/toolbarconfigurator/css/fontello.css b/sources/samples/toolbarconfigurator/css/fontello.css
new file mode 100644
index 00000000..bb32199d
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/css/fontello.css
@@ -0,0 +1,55 @@
1@font-face {
2 font-family: 'fontello';
3 src: url('../font/fontello.eot?89024372');
4 src: url('../font/fontello.eot?89024372#iefix') format('embedded-opentype'),
5 url('../font/fontello.woff?89024372') format('woff'),
6 url('../font/fontello.ttf?89024372') format('truetype'),
7 url('../font/fontello.svg?89024372#fontello') format('svg');
8 font-weight: normal;
9 font-style: normal;
10}
11/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
12/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
13/*
14@media screen and (-webkit-min-device-pixel-ratio:0) {
15 @font-face {
16 font-family: 'fontello';
17 src: url('../font/fontello.svg?89024372#fontello') format('svg');
18 }
19}
20*/
21
22 [class^="icon-"]:before, [class*=" icon-"]:before {
23 font-family: "fontello";
24 font-style: normal;
25 font-weight: normal;
26 speak: none;
27
28 display: inline-block;
29 text-decoration: inherit;
30 width: 1em;
31 margin-right: .2em;
32 text-align: center;
33 /* opacity: .8; */
34
35 /* For safety - reset parent styles, that can break glyph codes*/
36 font-variant: normal;
37 text-transform: none;
38
39 /* fix buttons height, for twitter bootstrap */
40 line-height: 1em;
41
42 /* Animation center compensation - margins should be symmetric */
43 /* remove if not needed */
44 margin-left: .2em;
45
46 /* you can be more comfortable with increased icons size */
47 /* font-size: 120%; */
48
49 /* Uncomment for 3D effect */
50 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
51}
52
53.icon-trash:before { content: '\e802'; } /* '' */
54.icon-down-big:before { content: '\e800'; } /* '' */
55.icon-up-big:before { content: '\e801'; } /* '' */ \ No newline at end of file
diff --git a/sources/samples/toolbarconfigurator/font/LICENSE.txt b/sources/samples/toolbarconfigurator/font/LICENSE.txt
new file mode 100644
index 00000000..b5110545
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/LICENSE.txt
@@ -0,0 +1,12 @@
1Font license info
2
3
4## Font Awesome
5
6 Copyright (C) 2012 by Dave Gandy
7
8 Author: Dave Gandy
9 License: SIL ()
10 Homepage: http://fortawesome.github.com/Font-Awesome/
11
12
diff --git a/sources/samples/toolbarconfigurator/font/config.json b/sources/samples/toolbarconfigurator/font/config.json
new file mode 100644
index 00000000..94809d70
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/config.json
@@ -0,0 +1,28 @@
1{
2 "name": "",
3 "css_prefix_text": "icon-",
4 "css_use_suffix": false,
5 "hinting": true,
6 "units_per_em": 1000,
7 "ascent": 850,
8 "glyphs": [
9 {
10 "uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
11 "css": "trash-empty",
12 "code": 59392,
13 "src": "fontawesome"
14 },
15 {
16 "uid": "1c4068ed75209e21af36017df8871802",
17 "css": "down-big",
18 "code": 59393,
19 "src": "fontawesome"
20 },
21 {
22 "uid": "95376bf082bfec6ce06ea1cda7bd7ead",
23 "css": "up-big",
24 "code": 59394,
25 "src": "fontawesome"
26 }
27 ]
28} \ No newline at end of file
diff --git a/sources/samples/toolbarconfigurator/font/fontello.eot b/sources/samples/toolbarconfigurator/font/fontello.eot
new file mode 100644
index 00000000..2732fad4
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/fontello.eot
Binary files differ
diff --git a/sources/samples/toolbarconfigurator/font/fontello.svg b/sources/samples/toolbarconfigurator/font/fontello.svg
new file mode 100644
index 00000000..33d14ac8
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/fontello.svg
@@ -0,0 +1,14 @@
1<?xml version="1.0" standalone="no"?>
2<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3<svg xmlns="http://www.w3.org/2000/svg">
4<metadata>Copyright (C) 2014 by original authors @ fontello.com</metadata>
5<defs>
6<font id="fontello" horiz-adv-x="1000" >
7<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
8<missing-glyph horiz-adv-x="1000" />
9<glyph glyph-name="trash" unicode="&#xe802;" d="m286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15t6-5h464q2 0 6 5t8 15t4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q22 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
10<glyph glyph-name="down-big" unicode="&#xe800;" d="m899 386q0-30-21-50l-363-364q-22-21-51-21q-29 0-50 21l-363 364q-21 20-21 50q0 29 21 51l41 41q22 21 51 21q29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l164 164q21 21 51 21q29 0 50-21l42-42q21-21 21-50z" horiz-adv-x="928.6" />
11<glyph glyph-name="up-big" unicode="&#xe801;" d="m899 308q0-28-21-50l-42-42q-21-21-50-21q-30 0-51 21l-164 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50q0 30 21 51l363 363q20 21 50 21q30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
12</font>
13</defs>
14</svg> \ No newline at end of file
diff --git a/sources/samples/toolbarconfigurator/font/fontello.ttf b/sources/samples/toolbarconfigurator/font/fontello.ttf
new file mode 100644
index 00000000..fbcbf06a
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/fontello.ttf
Binary files differ
diff --git a/sources/samples/toolbarconfigurator/font/fontello.woff b/sources/samples/toolbarconfigurator/font/fontello.woff
new file mode 100644
index 00000000..e1d56472
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/font/fontello.woff
Binary files differ
diff --git a/sources/samples/toolbarconfigurator/index.html b/sources/samples/toolbarconfigurator/index.html
new file mode 100644
index 00000000..3c1cf525
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/index.html
@@ -0,0 +1,446 @@
1<!DOCTYPE html>
2<!--
3Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
4For licensing, see LICENSE.md or http://ckeditor.com/license
5-->
6<!--[if IE 8]><html class="ie8"><![endif]-->
7<!--[if gt IE 8]><html><![endif]-->
8<!--[if !IE]><!--><html><!--<![endif]-->
9<head>
10 <meta charset="utf-8">
11 <title>Toolbar Configurator</title>
12 <script src="../../ckeditor.js"></script>
13 <script>
14 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 )
15 CKEDITOR.tools.enableHtml5Elements( document );
16 </script>
17 <link rel="stylesheet" href="lib/codemirror/codemirror.css">
18 <link rel="stylesheet" href="lib/codemirror/show-hint.css">
19 <link rel="stylesheet" href="lib/codemirror/neo.css">
20 <link rel="stylesheet" href="css/fontello.css">
21 <link rel="stylesheet" href="../css/samples.css">
22</head>
23<body id="toolbar">
24
25<nav class="navigation-a">
26 <div class="grid-container">
27 <ul class="navigation-a-left grid-width-70">
28 <li><a href="http://ckeditor.com">Project Homepage</a></li>
29 <li><a href="http://dev.ckeditor.com/">I found a bug</a></li>
30 <li><a href="http://github.com/ckeditor/ckeditor-dev" class="icon-pos-right icon-navigation-a-github">Fork CKEditor on GitHub</a></li>
31 </ul>
32 <ul class="navigation-a-right grid-width-30">
33 <li><a href="http://ckeditor.com/blog-list">CKEditor Blog</a></li>
34 </ul>
35 </div>
36</nav>
37
38<header class="header-a">
39 <div class="grid-container">
40 <h1 class="header-a-logo grid-width-30">
41 <a href="../index.html"><img src="../img/logo.png" alt="CKEditor Logo"></a>
42 </h1>
43 <nav class="navigation-b grid-width-70">
44 <ul>
45 <li><a href="../index.html" class="button-a">Start</a></li>
46 <li><a href="index.html" class="button-a button-a-background">Toolbar configurator</a></li>
47 </ul>
48 </nav>
49 </div>
50</header>
51
52<main>
53 <div class="adjoined-top">
54 <div class="grid-container">
55 <div class="content grid-width-100">
56 <div class="grid-container-nested">
57 <h1 class="grid-width-60">
58 Toolbar Configurator
59 <a href="#help-content" type="button" title="Configurator help" id="help" class="button-a button-a-background button-a-no-text icon-pos-left icon-question-mark">Help</a>
60 </h1>
61
62 <div class="grid-width-40 grid-switch-magic">
63 <div class="switch">
64 <span class="balloon-a balloon-a-se">Select configurator type</span>
65 <input type="radio" name="radio" data-num="1" id="radio-basic" />
66 <input type="radio" name="radio" data-num="2" id="radio-advanced" />
67 <label data-for="1" for="radio-basic">Basic</label>
68 <span class="switch-inner">
69 <span class="handler"></span>
70 </span>
71 <label data-for="2" for="radio-advanced">Advanced</label>
72 </div>
73 </div>
74 </div>
75 </div>
76 </div>
77 </div>
78 <div class="adjoined-bottom">
79 <div class="grid-container">
80 <div class="grid-width-100">
81 <div class="editors-container">
82 <div id="editor-basic"></div>
83 <div id="editor-advanced"></div>
84 </div>
85 </div>
86 </div>
87 </div>
88
89 <div class="grid-container configurator">
90 <div class="content grid-width-100">
91 <div class="configurator">
92 <div>
93 <div id="toolbarModifierWrapper"></div>
94 </div>
95 </div>
96 </div>
97 </div>
98
99 <div id="help-content">
100 <div class="grid-container">
101 <div class="grid-width-100">
102 <h2>What Am I Doing Here?</h2>
103
104 <div class="grid-container grid-container-nested">
105 <div class="basic">
106 <div class="grid-width-50">
107 <p>Arrange <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbarGroups">toolbar groups</a>, toggle <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-removeButtons">button visibility</a> according to your needs and get your toolbar configuration.</p>
108 <p>You can replace the content of the <a href="../../config.js"><code>config.js</code></a> file with the generated configuration. If you already set some configuration options you will need to merge both configurations.</p>
109 </div>
110 <div class="grid-width-50">
111 <p>Read more about different ways of <a href="http://docs.ckeditor.com/#!/guide/dev_configuration">setting configuration</a> and do not forget about <strong>clearing browser cache</strong>.</p>
112 <p>Arranging toolbar groups is the recommended way of configuring the toolbar, but if you need more freedom you can use the <a href="#advanced">advanced configurator</a>.</p>
113 </div>
114 </div>
115 <div class="advanced" style="display: none;">
116 <div class="grid-width-50">
117 <p>With this code editor you can edit your <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-toolbar">toolbar configuration</a> live.</p>
118 <p>You can replace the content of the <a href="../../config.js"><code>config.js</code></a> file with the generated configuration. If you already set some configuration options you will need to merge both configurations.</p>
119 </div>
120 <div class="grid-width-50">
121 <p>Read more about different ways of <a href="http://docs.ckeditor.com/#!/guide/dev_configuration">setting configuration</a> and do not forget about <strong>clearing browser cache</strong>.</p>
122 </div>
123 </div>
124 </div>
125
126 <p class="grid-container grid-container-nested">
127 <button type="button" class="help-content-close grid-width-100 button-a button-a-background">Got it. Let's play!</button>
128 </p>
129 </div>
130 </div>
131 </div>
132</main>
133
134<footer class="footer-a grid-container">
135 <p class="grid-width-100">
136 CKEditor &ndash; The text editor for the Internet &ndash; <a class="samples" href="http://ckeditor.com/">http://ckeditor.com</a>
137 </p>
138 <p class="grid-width-100" id="copy">
139 Copyright &copy; 2003-2015, <a class="samples" href="http://cksource.com/">CKSource</a> &ndash; Frederico Knabben. All rights reserved.
140 </p>
141</footer>
142
143<script src="lib/codemirror/codemirror.js"></script>
144<script src="lib/codemirror/javascript.js"></script>
145<script src="lib/codemirror/show-hint.js"></script>
146
147<script src="js/fulltoolbareditor.js"></script>
148<script src="js/abstracttoolbarmodifier.js"></script>
149<script src="js/toolbarmodifier.js"></script>
150<script src="js/toolbartextmodifier.js"></script>
151<script src="../js/sf.js"></script>
152
153<script>
154 ( function() {
155 'use strict';
156
157 var mode = ( window.location.hash.substr( 1 ) === 'advanced' ) ? 'advanced' : 'basic',
158 configuratorSection = CKEDITOR.document.findOne( 'main > .grid-container.configurator' ),
159 basicInstruction = CKEDITOR.document.findOne( '#help-content .basic' ),
160 advancedInstruction = CKEDITOR.document.findOne( '#help-content .advanced' ),
161
162 // Configurator mode switcher.
163 modeSwitchBasic = CKEDITOR.document.getById( 'radio-basic' ),
164 modeSwitchAdvanced = CKEDITOR.document.getById( 'radio-advanced' );
165
166 // Initial setup
167 function updateSwitcher() {
168 if ( mode === 'advanced' ) {
169 modeSwitchAdvanced.$.checked = true;
170 } else {
171 modeSwitchBasic.$.checked = true;
172 }
173 }
174
175 updateSwitcher();
176
177 CKEDITOR.document.getWindow().on( 'hashchange', function( e ) {
178 var hash = window.location.hash.substr( 1 );
179 if ( !( hash === 'advanced' || hash === 'basic' ) ) {
180 return;
181 }
182 mode = hash;
183 onToolbarsDone( mode );
184 } );
185
186 CKEDITOR.document.getWindow().on( 'resize', function() {
187 updateToolbar( ( mode === 'basic' ? toolbarModifier : toolbarTextModifier )[ 'editorInstance' ] );
188 } );
189
190 function onRefresh( modifier ) {
191 modifier = modifier || this;
192
193 if ( mode === 'basic' && modifier instanceof ToolbarConfigurator.ToolbarTextModifier ) {
194 return;
195 }
196
197 // CodeMirror container becomes visible, so we need to refresh and to avoid rendering problems.
198 if ( mode === 'advanced' && modifier instanceof ToolbarConfigurator.ToolbarTextModifier ) {
199 modifier.codeContainer.refresh();
200 }
201
202 updateToolbar( modifier.editorInstance );
203 }
204
205 function updateToolbar( editor ) {
206 var editorContainer = editor.container;
207
208 // Not always editor is loaded.
209 if ( !editorContainer ) {
210 return;
211 }
212
213 var displayStyle = editorContainer.getStyle( 'display' );
214
215 editorContainer.setStyle( 'display', 'block' );
216
217 var newHeight = editorContainer.getSize( 'height' );
218
219 var newMarginTop = parseInt( editorContainer.getComputedStyle( 'margin-top' ), 10 );
220 newMarginTop = ( isNaN( newMarginTop ) ? 0 : Number( newMarginTop ) );
221
222 var newMarginBottom = parseInt( editorContainer.getComputedStyle( 'margin-bottom' ), 10 );
223 newMarginBottom = ( isNaN( newMarginBottom ) ? 0 : Number( newMarginBottom ) );
224
225 var result = newHeight + newMarginTop + newMarginBottom;
226
227 editorContainer.setStyle( 'display', displayStyle );
228
229 editor.container.getAscendant( 'div' ).setStyle( 'height', result + 'px' );
230 }
231
232 var toolbarModifier = new ToolbarConfigurator.ToolbarModifier( 'editor-basic' );
233
234 var done = 0;
235 toolbarModifier.init( onToolbarInit );
236 toolbarModifier.onRefresh = onRefresh;
237
238 CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarModifier.mainContainer );
239
240 var toolbarTextModifier = new ToolbarConfigurator.ToolbarTextModifier( 'editor-advanced' );
241 toolbarTextModifier.init( onToolbarInit );
242 toolbarTextModifier.onRefresh = onRefresh;
243
244 function onToolbarInit() {
245 if ( ++done === 2 ) {
246 onToolbarsDone();
247
248 positionSticky.watch( CKEDITOR.document.findOne( '.toolbar' ), function() {
249 return mode === 'advanced';
250 } );
251 }
252 }
253
254 function onToolbarsDone() {
255 if ( mode === 'basic' ) {
256 toggleModeBasic( false );
257 } else {
258 toggleModeAdvanced( false );
259 }
260
261 updateSwitcher();
262
263 setTimeout( function() {
264 CKEDITOR.document.findOne( '.editors-container' ).addClass( 'active' );
265 CKEDITOR.document.findOne( '#toolbarModifierWrapper' ).addClass( 'active' );
266 }, 200 );
267 }
268
269 CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarTextModifier.mainContainer );
270
271 function toogleModeSwitch( onElement, offElement, onModifier, offModifier ) {
272 onElement.addClass( 'fancy-button-active' );
273 offElement.removeClass( 'fancy-button-active' );
274
275 onModifier.showUI();
276 offModifier.hideUI();
277 }
278
279 function toggleModeBasic( callOnRefresh ) {
280 callOnRefresh = ( callOnRefresh !== false );
281 mode = 'basic';
282 window.location.hash = '#basic';
283 toogleModeSwitch( modeSwitchBasic, modeSwitchAdvanced, toolbarModifier, toolbarTextModifier );
284
285 configuratorSection.removeClass( 'freed-width' );
286 basicInstruction.show();
287 advancedInstruction.hide();
288
289 callOnRefresh && onRefresh( toolbarModifier );
290 }
291
292 function toggleModeAdvanced( callOnRefresh ) {
293 callOnRefresh = ( callOnRefresh !== false );
294 mode = 'advanced';
295 window.location.hash = '#advanced';
296 toogleModeSwitch( modeSwitchAdvanced, modeSwitchBasic, toolbarTextModifier, toolbarModifier );
297
298 configuratorSection.addClass( 'freed-width' );
299 advancedInstruction.show();
300 basicInstruction.hide();
301
302 callOnRefresh && onRefresh( toolbarTextModifier );
303 }
304
305 modeSwitchBasic.on( 'click', toggleModeBasic );
306 modeSwitchAdvanced.on( 'click', toggleModeAdvanced );
307
308 //
309 // Position:sticky for the toolbar.
310 //
311
312 // Will make elements behave like they were styled with position:sticky.
313 var positionSticky = {
314 // Store object: {
315 // element: CKEDITOR.dom.element, // Element which will float.
316 // placeholder: CKEDITOR.dom.element, // Placeholder which is place to prevent page bounce.
317 // isFixed: boolean // Whether element float now.
318 // }
319 watched: [],
320
321 active: [],
322
323 staticContainer: null,
324
325 init: function() {
326 var element = CKEDITOR.dom.element.createFromHtml(
327 '<div class="staticContainer">' +
328 '<div class="grid-container" >' +
329 '<div class="grid-width-100">' +
330 '<div class="inner"></div>' +
331 '</div>' +
332 '</div>' +
333 '</div>' );
334
335 this.staticContainer = element.findOne( '.inner' );
336
337 CKEDITOR.document.getBody().append( element );
338 },
339
340 watch: function( element, preventFunc ) {
341 this.watched.push( {
342 element: element,
343 placeholder: new CKEDITOR.dom.element( 'div' ),
344 isFixed: false,
345 preventFunc: preventFunc
346 } );
347 },
348
349 checkAll: function() {
350 for ( var i = 0; i < this.watched.length; i++ ) {
351 this.check( this.watched[ i ] );
352 }
353 },
354
355 check: function( element ) {
356 var isFixed = element.isFixed;
357 var shouldBeFixed = this.shouldBeFixed( element );
358
359 // Nothing to be done.
360 if ( isFixed === shouldBeFixed ) {
361 return;
362 }
363
364 var placeholder = element.placeholder;
365
366 if ( isFixed ) {
367 // Unfixing.
368
369 element.element.insertBefore( placeholder );
370 placeholder.remove();
371
372 element.element.removeStyle( 'margin' );
373
374 this.active.splice( CKEDITOR.tools.indexOf( this.active, element ), 1 );
375
376 } else {
377 // Fixing.
378 placeholder.setStyle( 'width', element.element.getSize( 'width' ) + 'px' );
379 placeholder.setStyle( 'height', element.element.getSize( 'height' ) + 'px' );
380 placeholder.setStyle( 'margin-bottom', element.element.getComputedStyle( 'margin-bottom' ) );
381 placeholder.setStyle( 'display', element.element.getComputedStyle( 'display' ) );
382 placeholder.insertAfter( element.element );
383
384 this.staticContainer.append( element.element );
385
386 this.active.push( element );
387 }
388
389 element.isFixed = !element.isFixed;
390 },
391
392 shouldBeFixed: function( element ) {
393 if ( element.preventFunc && element.preventFunc() ) {
394 return false;
395 }
396
397 // If element is already fixed we are checking it's placeholder.
398 var related = ( element.isFixed ? element.placeholder : element.element ),
399 clientRect = related.$.getBoundingClientRect(),
400 staticHeight = this.staticContainer.getSize('height' ),
401 elemHeight = element.element.getSize( 'height' );
402
403 if ( element.isFixed ) {
404 return ( clientRect.top + elemHeight < staticHeight );
405 } else {
406 return ( clientRect.top < staticHeight );
407 }
408 }
409 };
410
411 positionSticky.init();
412
413 CKEDITOR.document.getWindow().on( 'scroll',
414 new CKEDITOR.tools.eventsBuffer( 100, positionSticky.checkAll, positionSticky ).input
415 );
416
417 // Make the toolbar sticky.
418 positionSticky.watch( CKEDITOR.document.findOne( '.editors-container' ) );
419
420 // Help button and help-content.
421 ( function() {
422 var helpButton = CKEDITOR.document.getById( 'help' ),
423 helpContent = CKEDITOR.document.getById( 'help-content' );
424
425 // Don't show help button on IE8 because it's unsupported by Pico Modal.
426 if ( CKEDITOR.env.ie && CKEDITOR.env.version == 8 ) {
427 helpButton.hide();
428 } else {
429 // Display help modal when the button is clicked.
430 helpButton.on( 'click', function( evt ) {
431 SF.modal( {
432 // Clone modal content from DOM.
433 content: helpContent.getHtml(),
434
435 afterCreate: function( modal ) {
436 // Enable modal content button to close the modal.
437 new CKEDITOR.dom.element( modal.modalElem() ).findOne( '.help-content-close' ).once( 'click', modal.close );
438 }
439 } ).show();
440 } );
441 }
442 } )();
443 } )();
444</script>
445</body>
446</html>
diff --git a/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js b/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js
new file mode 100644
index 00000000..af6cd622
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/abstracttoolbarmodifier.js
@@ -0,0 +1,566 @@
1/* global ToolbarConfigurator */
2
3'use strict';
4
5// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
6if ( typeof Object.create != 'function' ) {
7 ( function() {
8 var F = function() {};
9 Object.create = function( o ) {
10 if ( arguments.length > 1 ) {
11 throw Error( 'Second argument not supported' );
12 }
13 if ( o === null ) {
14 throw Error( 'Cannot set a null [[Prototype]]' );
15 }
16 if ( typeof o != 'object' ) {
17 throw TypeError( 'Argument must be an object' );
18 }
19 F.prototype = o;
20 return new F();
21 };
22 } )();
23}
24
25// Copy of the divarea plugin (with some enhancements), so we always have some editable mode, regardless of the build's config.
26CKEDITOR.plugins.add( 'toolbarconfiguratorarea', {
27 // Use afterInit to override wysiwygarea's mode. May still fail to override divarea, but divarea is nice.
28 afterInit: function( editor ) {
29 editor.addMode( 'wysiwyg', function( callback ) {
30 var editingBlock = CKEDITOR.dom.element.createFromHtml( '<div class="cke_wysiwyg_div cke_reset" hidefocus="true"></div>' );
31
32 var contentSpace = editor.ui.space( 'contents' );
33 contentSpace.append( editingBlock );
34
35 editingBlock = editor.editable( editingBlock );
36
37 editingBlock.detach = CKEDITOR.tools.override( editingBlock.detach,
38 function( org ) {
39 return function() {
40 org.apply( this, arguments );
41 this.remove();
42 };
43 } );
44
45 editor.setData( editor.getData( 1 ), callback );
46 editor.fire( 'contentDom' );
47 } );
48
49 // Additions to the divarea.
50
51 // Speed up data processing.
52 editor.dataProcessor.toHtml = function( html ) {
53 return html;
54 };
55 editor.dataProcessor.toDataFormat = function( html ) {
56 return html;
57 };
58
59 // End of the additions.
60 }
61} );
62
63// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
64if ( !Object.keys ) {
65 Object.keys = ( function() {
66 var hasOwnProperty = Object.prototype.hasOwnProperty,
67 hasDontEnumBug = !( { toString: null } ).propertyIsEnumerable( 'toString' ),
68 dontEnums = [
69 'toString',
70 'toLocaleString',
71 'valueOf',
72 'hasOwnProperty',
73 'isPrototypeOf',
74 'propertyIsEnumerable',
75 'constructor'
76 ],
77 dontEnumsLength = dontEnums.length;
78
79 return function( obj ) {
80 if ( typeof obj !== 'object' && ( typeof obj !== 'function' || obj === null ) )
81 throw new TypeError( 'Object.keys called on non-object' );
82
83 var result = [], prop, i;
84
85 for ( prop in obj ) {
86 if ( hasOwnProperty.call( obj, prop ) )
87 result.push( prop );
88
89 }
90
91 if ( hasDontEnumBug ) {
92 for ( i = 0; i < dontEnumsLength; i++ ) {
93 if ( hasOwnProperty.call( obj, dontEnums[ i ] ) )
94 result.push( dontEnums[ i ] );
95
96 }
97 }
98 return result;
99 };
100 }() );
101}
102
103( function() {
104 /**
105 * @class ToolbarConfigurator.AbstractToolbarModifier
106 * @param {String} editorId An id of modified editor
107 * @constructor
108 */
109 function AbstractToolbarModifier( editorId, cfg ) {
110 this.cfg = cfg || {};
111 this.hidden = false;
112 this.editorId = editorId;
113 this.fullToolbarEditor = new ToolbarConfigurator.FullToolbarEditor();
114
115 this.mainContainer = null;
116
117 this.originalConfig = null;
118 this.actualConfig = null;
119
120 this.waitForReady = false;
121 this.isEditableVisible = false;
122
123 this.toolbarContainer = null;
124 this.toolbarButtons = [];
125 }
126
127 // Expose the class.
128 ToolbarConfigurator.AbstractToolbarModifier = AbstractToolbarModifier;
129
130 /**
131 * @param {String} config
132 */
133 AbstractToolbarModifier.prototype.setConfig = function( config ) {
134 this._onInit( undefined, config, true );
135 };
136
137 /**
138 * @param {Function} [callback]
139 */
140 AbstractToolbarModifier.prototype.init = function( callback ) {
141 var that = this;
142
143 this.mainContainer = new CKEDITOR.dom.element( 'div' );
144
145 if ( this.fullToolbarEditor.editorInstance !== null ) {
146 throw 'Only one instance of ToolbarModifier is allowed';
147 }
148
149 if ( !this.editorInstance ) {
150 // Do not refresh yet, let's wait for the full toolbar editor (see below).
151 this._createEditor( false );
152 }
153
154 this.editorInstance.once( 'loaded', function() {
155 that.fullToolbarEditor.init( function() {
156 that._onInit( callback );
157
158 if ( typeof that.onRefresh == 'function' ) {
159 that.onRefresh();
160 }
161 }, that.editorInstance.config );
162 } );
163
164 return this.mainContainer;
165 };
166
167 /**
168 * Called editor initialization finished.
169 *
170 * @param {Function} callback
171 * @param {String} [actualConfig]
172 * @private
173 */
174 AbstractToolbarModifier.prototype._onInit = function( callback, actualConfig ) {
175 this.originalConfig = this.editorInstance.config;
176
177 if ( !actualConfig ) {
178 this.actualConfig = JSON.parse( JSON.stringify( this.originalConfig ) );
179 } else {
180 this.actualConfig = JSON.parse( actualConfig );
181 }
182
183 if ( !this.actualConfig.toolbarGroups && !this.actualConfig.toolbar ) {
184 this.actualConfig.toolbarGroups = getDefaultToolbarGroups( this.editorInstance );
185 }
186
187 if ( typeof callback === 'function' )
188 callback( this.mainContainer );
189
190 // Here we are going to keep only `name` and `groups` data from editor `toolbar` property.
191 function getDefaultToolbarGroups( editor ) {
192 var toolbarGroups = editor.toolbar,
193 copy = [];
194
195 var max = toolbarGroups.length;
196 for ( var i = 0; i < max; i++ ) {
197 var group = toolbarGroups[ i ];
198
199 if ( typeof group == 'string' ) {
200 copy.push( group ); // separator
201 } else {
202 copy.push( {
203 name: group.name,
204 groups: group.groups ? group.groups.slice() : []
205 } );
206 }
207 }
208
209 return copy;
210 }
211 };
212
213 /**
214 * Creates DOM structure of tool.
215 *
216 * @returns {CKEDITOR.dom.element}
217 * @private
218 */
219 AbstractToolbarModifier.prototype._createModifier = function() {
220 this.mainContainer.addClass( 'unselectable' );
221
222 if ( this.modifyContainer ) {
223 this.modifyContainer.remove();
224 }
225
226 this.modifyContainer = new CKEDITOR.dom.element( 'div' );
227 this.modifyContainer.addClass( 'toolbarModifier' );
228
229 this.mainContainer.append( this.modifyContainer );
230
231 return this.mainContainer;
232 };
233
234 /**
235 * Find editable area in CKEditor instance DOM container
236 *
237 * @returns {CKEDITOR.dom.element}
238 */
239 AbstractToolbarModifier.prototype.getEditableArea = function() {
240 var selector = ( '#' + this.editorInstance.id + '_contents' );
241
242 return this.editorInstance.container.findOne( selector );
243 };
244
245 /**
246 * Hide editable area in modified editor by sets its height to 0.
247 *
248 * @private
249 */
250 AbstractToolbarModifier.prototype._hideEditable = function() {
251 var area = this.getEditableArea();
252
253 this.isEditableVisible = false;
254
255 this.lastEditableAreaHeight = area.getStyle( 'height' );
256 area.setStyle( 'height', '0' );
257 };
258
259 /**
260 * Show editable area in modified editor.
261 *
262 * @private
263 */
264 AbstractToolbarModifier.prototype._showEditable = function() {
265 this.isEditableVisible = true;
266
267 this.getEditableArea().setStyle( 'height', this.lastEditableAreaHeight || 'auto' );
268 };
269
270 /**
271 * Toggle editable area visibility.
272 *
273 * @private
274 */
275 AbstractToolbarModifier.prototype._toggleEditable = function() {
276 if ( this.isEditableVisible )
277 this._hideEditable();
278 else
279 this._showEditable();
280 };
281
282 /**
283 * Usually called when configuration changes.
284 *
285 * @private
286 */
287 AbstractToolbarModifier.prototype._refreshEditor = function() {
288 var that = this,
289 status = this.editorInstance.status;
290
291 // Wait for ready only once.
292 if ( this.waitForReady )
293 return;
294
295 // Not ready.
296 if ( status == 'unloaded' || status == 'loaded' ) {
297 this.waitForReady = true;
298
299 this.editorInstance.once( 'instanceReady', function() {
300 refresh();
301 }, this );
302 // Ready or destroyed.
303 } else {
304 refresh();
305 }
306
307 function refresh() {
308 that.editorInstance.destroy();
309 that._createEditor( true, that.getActualConfig() );
310 that.waitForReady = false;
311 }
312 };
313
314 /**
315 * Creates editor that can be used to present the toolbar configuration.
316 *
317 * @private
318 */
319 AbstractToolbarModifier.prototype._createEditor = function( doRefresh, configOverrides ) {
320 var that = this;
321
322 this.editorInstance = CKEDITOR.replace( this.editorId );
323
324 this.editorInstance.on( 'configLoaded', function() {
325 var config = that.editorInstance.config;
326
327 if ( configOverrides ) {
328 CKEDITOR.tools.extend( config, configOverrides, true );
329 }
330
331 AbstractToolbarModifier.extendPluginsConfig( config );
332 } );
333
334 // Prevent creating any other space than the top one.
335 this.editorInstance.on( 'uiSpace', function( evt ) {
336 if ( evt.data.space != 'top' ) {
337 evt.stop();
338 }
339 }, null, null, -999 );
340
341 this.editorInstance.once( 'loaded', function() {
342 var btns = that.editorInstance.ui.instances;
343
344 for ( var i in btns ) {
345 if ( btns[ i ] ) {
346 btns[ i ].click = empty;
347 btns[ i ].onClick = empty;
348 }
349 }
350
351 if ( !that.isEditableVisible ) {
352 that._hideEditable();
353 }
354
355 if ( that.currentActive && that.currentActive.name ) {
356 that._highlightGroup( that.currentActive.name );
357 }
358
359 if ( that.hidden ) {
360 that.hideUI();
361 } else {
362 that.showUI();
363 }
364
365 if ( doRefresh && ( typeof that.onRefresh === 'function' ) ) {
366 that.onRefresh();
367 }
368 } );
369
370 function empty() {}
371 };
372
373 /**
374 * Always returns copy of config.
375 *
376 * @returns {Object}
377 */
378 AbstractToolbarModifier.prototype.getActualConfig = function() {
379 return JSON.parse( JSON.stringify( this.actualConfig ) );
380 };
381
382 /**
383 * Creates toolbar in tool.
384 *
385 * @private
386 */
387 AbstractToolbarModifier.prototype._createToolbar = function() {
388 if ( !this.toolbarButtons.length ) {
389 return;
390 }
391
392 this.toolbarContainer = new CKEDITOR.dom.element( 'div' );
393 this.toolbarContainer.addClass( 'toolbar' );
394
395 var max = this.toolbarButtons.length;
396 for ( var i = 0; i < max; i += 1 ) {
397 this._createToolbarBtn( this.toolbarButtons[ i ] );
398 }
399 };
400
401 /**
402 * Create toolbar button and add it to toolbar container
403 *
404 * @param {Object} cfg
405 * @returns {CKEDITOR.dom.element}
406 * @private
407 */
408 AbstractToolbarModifier.prototype._createToolbarBtn = function( cfg ) {
409 var btnText = ( typeof cfg.text === 'string' ? cfg.text : cfg.text.inactive ),
410 btn = ToolbarConfigurator.FullToolbarEditor.createButton( btnText, cfg.cssClass );
411
412 this.toolbarContainer.append( btn );
413 btn.data( 'group', cfg.group );
414 btn.addClass( cfg.position );
415 btn.on( 'click', function() {
416 cfg.clickCallback.call( this, btn, cfg );
417 }, this );
418
419 return btn;
420 };
421
422 /**
423 * @private
424 * @param {Object} config
425 */
426 AbstractToolbarModifier.prototype._fixGroups = function( config ) {
427 var groups = config.toolbarGroups || [];
428
429 var max = groups.length;
430 for ( var i = 0; i < max; i += 1 ) {
431 var currentGroup = groups[ i ];
432
433 // separator, in config, is in raw format
434 // need to make it more sophisticated to keep unique id
435 // for each one
436 if ( currentGroup == '/' ) {
437 currentGroup = groups[ i ] = {};
438 currentGroup.type = 'separator';
439 currentGroup.name = ( 'separator' + CKEDITOR.tools.getNextNumber() );
440 continue;
441 }
442
443 // sometimes subgroups are not set (basic package), so need to
444 // create them artifically
445 currentGroup.groups = currentGroup.groups || [];
446
447 // when there is no subgroup with same name like its parent name
448 // then it have to be added artificially
449 // in order to maintain consistency between user interface and config
450 if ( CKEDITOR.tools.indexOf( currentGroup.groups, currentGroup.name ) == -1 ) {
451 this.editorInstance.ui.addToolbarGroup( currentGroup.name, currentGroup.groups[ currentGroup.groups.length - 1 ], currentGroup.name );
452 currentGroup.groups.push( currentGroup.name );
453 }
454
455 this._fixSubgroups( currentGroup );
456 }
457 };
458
459 /**
460 * Transform subgroup string to object literal
461 * with keys: {String} name and {Number} totalBtns
462 * Please note: this method modify Object provided in first argument
463 *
464 * input:
465 * [
466 * { groups: [ 'nameOne', 'nameTwo' ] }
467 * ]
468 *
469 * output:
470 * [
471 * { groups: [ { name: 'nameOne', totalBtns: 3 }, { name: 'nameTwo', totalBtns: 5 } ] }
472 * ]
473 *
474 * @param {Object} group
475 * @private
476 */
477 AbstractToolbarModifier.prototype._fixSubgroups = function( group ) {
478 var subGroups = group.groups;
479
480 var max = subGroups.length;
481 for ( var i = 0; i < max; i += 1 ) {
482 var subgroupName = subGroups[ i ];
483
484 subGroups[ i ] = {
485 name: subgroupName,
486 totalBtns: ToolbarConfigurator.ToolbarModifier.getTotalSubGroupButtonsNumber( subgroupName, this.fullToolbarEditor )
487 };
488 }
489 };
490
491 /**
492 * Same as JSON.stringify method but returned string is in one line
493 *
494 * @param {Object} json
495 * @param {Object} opts
496 * @param {Boolean} opts.addSpaces
497 * @param {Boolean} opts.noQuotesOnKey
498 * @param {Boolean} opts.singleQuotes
499 * @returns {Object}
500 */
501 AbstractToolbarModifier.stringifyJSONintoOneLine = function( json, opts ) {
502 opts = opts || {};
503 var stringJSON = JSON.stringify( json, null, '' );
504
505 // IE8 make new line characters
506 stringJSON = stringJSON.replace( /\n/g, '' );
507
508 if ( opts.addSpaces ) {
509 stringJSON = stringJSON.replace( /(\{|:|,|\[|\])/g, function( sentence ) {
510 return sentence + ' ';
511 } );
512
513 stringJSON = stringJSON.replace( /(\])/g, function( sentence ) {
514 return ' ' + sentence;
515 } );
516 }
517
518 if ( opts.noQuotesOnKey ) {
519 stringJSON = stringJSON.replace( /"(\w*)":/g, function( sentence, word ) {
520 return word + ':';
521 } );
522 }
523
524 if ( opts.singleQuotes ) {
525 stringJSON = stringJSON.replace( /\"/g, '\'' );
526 }
527
528 return stringJSON;
529 };
530
531 /**
532 * Hide toolbar configurator
533 */
534 AbstractToolbarModifier.prototype.hideUI = function() {
535 this.hidden = true;
536 this.mainContainer.hide();
537 if ( this.editorInstance.container ) {
538 this.editorInstance.container.hide();
539 }
540 };
541
542 /**
543 * Show toolbar configurator
544 */
545 AbstractToolbarModifier.prototype.showUI = function() {
546 this.hidden = false;
547 this.mainContainer.show();
548 if ( this.editorInstance.container ) {
549 this.editorInstance.container.show();
550 }
551 };
552
553
554 /**
555 * Extends plugins setttings in the specified config with settings useful for
556 * the toolbar configurator.
557 *
558 * @static
559 */
560 AbstractToolbarModifier.extendPluginsConfig = function( config ) {
561 var extraPlugins = config.extraPlugins;
562
563 // Enable the special, lightweight area to replace wysiwygarea.
564 config.extraPlugins = ( extraPlugins ? extraPlugins + ',' : '' ) + 'toolbarconfiguratorarea';
565 };
566} )();
diff --git a/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js b/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js
new file mode 100644
index 00000000..2b9f15f7
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/fulltoolbareditor.js
@@ -0,0 +1,365 @@
1/* exported ToolbarConfigurator */
2/* global ToolbarConfigurator */
3
4'use strict';
5
6window.ToolbarConfigurator = {};
7
8( function() {
9 /**
10 * @class ToolbarConfigurator.FullToolbarEditor
11 * @constructor
12 */
13 function FullToolbarEditor() {
14 this.instanceid = 'fte' + CKEDITOR.tools.getNextId();
15 this.textarea = new CKEDITOR.dom.element( 'textarea' );
16 this.textarea.setAttributes( {
17 id: this.instanceid,
18 name: this.instanceid,
19 contentEditable: true
20 } );
21
22 this.buttons = null;
23 this.editorInstance = null;
24 }
25
26 // Expose the class.
27 ToolbarConfigurator.FullToolbarEditor = FullToolbarEditor;
28
29 /**
30 * @param {Function} callback
31 * @param {Object} cfg
32 */
33 FullToolbarEditor.prototype.init = function( callback ) {
34 var that = this;
35
36 document.body.appendChild( this.textarea.$ );
37
38 CKEDITOR.replace( this.instanceid );
39
40 this.editorInstance = CKEDITOR.instances[ this.instanceid ];
41
42 this.editorInstance.once( 'configLoaded', function( e ) {
43 var cfg = e.editor.config;
44
45 // We want all the buttons.
46 delete cfg.removeButtons;
47 delete cfg.toolbarGroups;
48 delete cfg.toolbar;
49 ToolbarConfigurator.AbstractToolbarModifier.extendPluginsConfig( cfg );
50
51 e.editor.once( 'loaded', function() {
52 that.buttons = FullToolbarEditor.toolbarToButtons( that.editorInstance.toolbar );
53
54 that.buttonsByGroup = FullToolbarEditor.groupButtons( that.buttons );
55
56 that.buttonNamesByGroup = that.groupButtonNamesByGroup( that.buttons );
57
58 e.editor.container.hide();
59
60 if ( typeof callback === 'function' )
61 callback( that.buttons );
62 } );
63 } );
64 };
65
66 /**
67 * Group array of button names by their group parents.
68 *
69 * @param {Array} buttons
70 * @returns {Object}
71 */
72 FullToolbarEditor.prototype.groupButtonNamesByGroup = function( buttons ) {
73 var that = this,
74 groups = FullToolbarEditor.groupButtons( buttons );
75
76 for ( var groupName in groups ) {
77 var currGroup = groups[ groupName ];
78
79 groups[ groupName ] = FullToolbarEditor.map( currGroup, function( button ) {
80 return that.getCamelCasedButtonName( button.name );
81 } );
82 }
83
84 return groups;
85 };
86
87 /**
88 * Returns group literal.
89 *
90 * @param {String} name
91 * @returns {Object}
92 */
93 FullToolbarEditor.prototype.getGroupByName = function( name ) {
94 var groups = this.editorInstance.config.toolbarGroups || this.getFullToolbarGroupsConfig();
95
96 var max = groups.length;
97 for ( var i = 0; i < max; i += 1 ) {
98 if ( groups[ i ].name === name )
99 return groups[ i ];
100 }
101
102 return null;
103 };
104
105 /**
106 * @param {String} name
107 * @returns {String | null}
108 */
109 FullToolbarEditor.prototype.getCamelCasedButtonName = function( name ) {
110 var items = this.editorInstance.ui.items;
111
112 for ( var key in items ) {
113 if ( items[ key ].name == name )
114 return key;
115 }
116
117 return null;
118 };
119
120 /**
121 * Returns full toolbarGroups config value which is used when
122 * there is no toolbarGroups field in config.
123 *
124 * @param {Boolean} [pickSeparators=false]
125 * @returns {Array}
126 */
127 FullToolbarEditor.prototype.getFullToolbarGroupsConfig = function( pickSeparators ) {
128 pickSeparators = ( pickSeparators === true ? true : false );
129
130 var result = [],
131 toolbarGroups = this.editorInstance.toolbar;
132
133 var max = toolbarGroups.length;
134 for ( var i = 0; i < max; i += 1 ) {
135 var currentGroup = toolbarGroups[ i ],
136 copiedGroup = {};
137
138 if ( typeof currentGroup.name != 'string' ) {
139 // this is not a group
140 if ( pickSeparators ) {
141 result.push( '/' );
142 }
143 continue;
144 }
145
146 copiedGroup.name = currentGroup.name;
147 if ( currentGroup.groups )
148 copiedGroup.groups = Array.prototype.slice.call( currentGroup.groups );
149
150 result.push( copiedGroup );
151 }
152
153 return result;
154 };
155
156 /**
157 * Filters array items based on checker provided in second argument.
158 * Returns new array.
159 *
160 * @param {Array} arr
161 * @param {Function} checker
162 * @returns {Array}
163 */
164 FullToolbarEditor.filter = function( arr, checker ) {
165 var max = ( arr && arr.length ? arr.length : 0 ),
166 result = [];
167
168 for ( var i = 0; i < max; i += 1 ) {
169 if ( checker( arr[ i ] ) )
170 result.push( arr[ i ] );
171 }
172
173 return result;
174 };
175
176 /**
177 * Simplified http://underscorejs.org/#map functionality
178 *
179 * @param {Array | Object} enumerable
180 * @param {Function} modifier
181 * @returns {Array | Object}
182 */
183 FullToolbarEditor.map = function( enumerable, modifier ) {
184 var result;
185
186 if ( CKEDITOR.tools.isArray( enumerable ) ) {
187 result = [];
188
189 var max = enumerable.length;
190 for ( var i = 0; i < max; i += 1 )
191 result.push( modifier( enumerable[ i ] ) );
192 } else {
193 result = {};
194
195 for ( var key in enumerable )
196 result[ key ] = modifier( enumerable[ key ] );
197 }
198
199 return result;
200 };
201
202 /**
203 * Group buttons by their parent names.
204 *
205 * @static
206 * @param {Array} buttons
207 * @returns {Object} The object (`name => group`) representing CKEDITOR.ui.button or CKEDITOR.ui.richCombo
208 */
209 FullToolbarEditor.groupButtons = function( buttons ) {
210 var groups = {};
211
212 var max = buttons.length;
213 for ( var i = 0; i < max; i += 1 ) {
214 var currBtn = buttons[ i ],
215 currBtnGroupName = currBtn.toolbar.split( ',' )[ 0 ];
216
217 groups[ currBtnGroupName ] = groups[ currBtnGroupName ] || [];
218
219 groups[ currBtnGroupName ].push( currBtn );
220 }
221
222 return groups;
223 };
224
225 /**
226 * Pick all buttons from toolbar.
227 *
228 * @static
229 * @param {Array} groups
230 * @returns {Array}
231 */
232 FullToolbarEditor.toolbarToButtons = function( groups ) {
233 var buttons = [];
234
235 var max = groups.length;
236 for ( var i = 0; i < max; i += 1 ) {
237 var currentGroup = groups[ i ];
238
239 if ( typeof currentGroup == 'object' )
240 buttons = buttons.concat( FullToolbarEditor.groupToButtons( groups[ i ] ) );
241 }
242
243 return buttons;
244 };
245
246 /**
247 * Creates HTML button representation for view.
248 *
249 * @static
250 * @param {CKEDITOR.ui.button | CKEDITOR.ui.richCombo} button
251 * @returns {CKEDITOR.dom.element}
252 */
253 FullToolbarEditor.createToolbarButton = function( button ) {
254 var $button = new CKEDITOR.dom.element( 'a' ),
255 icon = FullToolbarEditor.createIcon( button.name, button.icon, button.command );
256
257 $button.setStyle( 'float', 'none' );
258
259 $button.addClass( 'cke_' + ( CKEDITOR.lang.dir == 'rtl' ? 'rtl' : 'ltr' ) );
260
261 if ( button instanceof CKEDITOR.ui.button ) {
262 $button.addClass( 'cke_button' );
263 $button.addClass( 'cke_toolgroup' );
264
265 $button.append( icon );
266 } else if ( CKEDITOR.ui.richCombo && button instanceof CKEDITOR.ui.richCombo ) {
267 var comboLabel = new CKEDITOR.dom.element( 'span' ),
268 comboOpen = new CKEDITOR.dom.element( 'span' ),
269 comboArrow = new CKEDITOR.dom.element( 'span' );
270
271 $button.addClass( 'cke_combo_button' );
272
273 comboLabel.addClass( 'cke_combo_text' );
274 comboLabel.addClass( 'cke_combo_inlinelabel' );
275 comboLabel.setText( button.label );
276
277 comboOpen.addClass( 'cke_combo_open' );
278 comboArrow.addClass( 'cke_combo_arrow' );
279 comboOpen.append( comboArrow );
280
281 $button.append( comboLabel );
282 $button.append( comboOpen );
283 }
284
285 return $button;
286 };
287
288 /**
289 * Create and return icon element.
290 *
291 * @param {String} name
292 * @param {String} icon
293 * @param {String} command
294 * @static
295 * @returns {CKEDITOR.dom.element}
296 */
297 FullToolbarEditor.createIcon = function( name, icon, command ) {
298 var iconStyle = CKEDITOR.skin.getIconStyle( name, ( CKEDITOR.lang.dir == 'rtl' ) );
299
300 // We don't know exactly how to get icon style. Especially for extra plugins,
301 // Which definition may vary.
302 iconStyle = iconStyle || CKEDITOR.skin.getIconStyle( icon, ( CKEDITOR.lang.dir == 'rtl' ) );
303 iconStyle = iconStyle || CKEDITOR.skin.getIconStyle( command, ( CKEDITOR.lang.dir == 'rtl' ) );
304
305 var iconElement = new CKEDITOR.dom.element( 'span' );
306
307 iconElement.addClass( 'cke_button_icon' );
308 iconElement.addClass( 'cke_button__' + name + '_icon' );
309 iconElement.setAttribute( 'style', iconStyle );
310 iconElement.setStyle( 'float', 'none' );
311
312 return iconElement;
313 };
314
315 /**
316 * Create and return button element
317 *
318 * @param {String} text
319 * @param {String} cssClasses
320 * @returns {CKEDITOR.dom.element}
321 */
322 FullToolbarEditor.createButton = function( text, cssClasses ) {
323 var $button = new CKEDITOR.dom.element( 'button' );
324
325 $button.addClass( 'button-a' );
326
327 $button.setAttribute( 'type', 'button' );
328
329 if ( typeof cssClasses == 'string' ) {
330 cssClasses = cssClasses.split( ' ' );
331
332 var i = cssClasses.length;
333 while ( i-- ) {
334 $button.addClass( cssClasses[ i ] );
335 }
336 }
337
338 $button.setHtml( text );
339
340 return $button;
341 };
342
343 /**
344 * @static
345 * @param {Object} group
346 * @returns {Array} representing HTML buttons for view
347 */
348 FullToolbarEditor.groupToButtons = function( group ) {
349 var buttons = [],
350 items = group.items;
351
352 var max = items ? items.length : 0;
353 for ( var i = 0; i < max; i += 1 ) {
354 var item = items[ i ];
355
356 if ( item instanceof CKEDITOR.ui.button || CKEDITOR.ui.richCombo && item instanceof CKEDITOR.ui.richCombo ) {
357 item.$ = FullToolbarEditor.createToolbarButton( item );
358 buttons.push( item );
359 }
360 }
361
362 return buttons;
363 };
364
365} )();
diff --git a/sources/samples/toolbarconfigurator/js/toolbarmodifier.js b/sources/samples/toolbarconfigurator/js/toolbarmodifier.js
new file mode 100644
index 00000000..bd33d24f
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/toolbarmodifier.js
@@ -0,0 +1,1366 @@
1/* global ToolbarConfigurator, alert */
2
3'use strict';
4
5( function() {
6 var AbstractToolbarModifier = ToolbarConfigurator.AbstractToolbarModifier;
7
8 /**
9 * @class ToolbarConfigurator.ToolbarModifier
10 * @param {String} editorId An id of modified editor
11 * @param {Object} cfg
12 * @extends AbstractToolbarModifier
13 * @constructor
14 */
15 function ToolbarModifier( editorId, cfg ) {
16 AbstractToolbarModifier.call( this, editorId, cfg );
17
18 this.removedButtons = null;
19 this.originalConfig = null;
20 this.actualConfig = null;
21 this.emptyVisible = false;
22
23 // edit, paste, config
24 this.state = 'edit';
25
26 this.toolbarButtons = [
27 {
28 text: {
29 active: 'Hide empty toolbar groups',
30 inactive: 'Show empty toolbar groups'
31 },
32 group: 'edit',
33 position: 'left',
34 cssClass: 'button-a-soft',
35 clickCallback: function( button, buttonDefinition ) {
36 var className = 'button-a-background';
37
38 button[ button.hasClass( className ) ? 'removeClass' : 'addClass' ]( className );
39
40 this._toggleVisibilityEmptyElements();
41
42 if ( this.emptyVisible ) {
43 button.setText( buttonDefinition.text.active );
44 } else {
45 button.setText( buttonDefinition.text.inactive );
46 }
47 }
48 },
49 {
50 text: 'Add row separator',
51 group: 'edit',
52 position: 'left',
53 cssClass: 'button-a-soft',
54 clickCallback: function() {
55 this._addSeparator();
56 }
57 },
58 /*{
59 text: 'Paste config',
60 group: 'edit',
61 position: 'left',
62 clickCallback: function() {
63 this.state = 'paste';
64
65 this.modifyContainer.addClass( 'hidden' );
66 this.configContainer.removeClass( 'hidden' );
67 this.configContainer.setHtml( '<textarea></textarea>' );
68 this.showToolbarBtnsByGroupName( 'config' );
69 }
70 },*/
71 {
72 text: 'Select config',
73 group: 'config',
74 position: 'left',
75 cssClass: 'button-a-soft',
76 clickCallback: function() {
77 this.configContainer.findOne( 'textarea' ).$.select();
78 }
79 },
80 {
81 text: 'Back to configurator',
82 group: 'config',
83 position: 'right',
84 cssClass: 'button-a-background',
85 clickCallback: function() {
86 if ( this.state === 'paste' ) {
87 var cfg = this.configContainer.findOne( 'textarea' ).getValue();
88 cfg = ToolbarModifier.evaluateToolbarGroupsConfig( cfg );
89
90 if ( cfg ) {
91 this.setConfig( cfg );
92 } else {
93 alert( 'Your pasted config is wrong.' );
94 }
95 }
96
97 this.state = 'edit';
98 this._showConfigurationTool();
99 this.showToolbarBtnsByGroupName( this.state );
100 }
101 },
102 {
103 text: 'Get toolbar <span class="highlight">config</span>',
104 group: 'edit',
105 position: 'right',
106 cssClass: 'button-a-background icon-pos-left icon-download',
107 clickCallback: function() {
108 this.state = 'config';
109 this._showConfig();
110 this.showToolbarBtnsByGroupName( this.state );
111 }
112 }
113 ];
114
115 this.cachedActiveElement = null;
116 }
117
118 // Expose the class.
119 ToolbarConfigurator.ToolbarModifier = ToolbarModifier;
120
121 ToolbarModifier.prototype = Object.create( ToolbarConfigurator.AbstractToolbarModifier.prototype );
122
123 /**
124 * @returns {Object}
125 */
126 ToolbarModifier.prototype.getActualConfig = function() {
127 var copy = AbstractToolbarModifier.prototype.getActualConfig.call( this );
128
129 if ( copy.toolbarGroups ) {
130
131 var max = copy.toolbarGroups.length;
132 for ( var i = 0; i < max; i += 1 ) {
133 var currentGroup = copy.toolbarGroups[ i ];
134
135 copy.toolbarGroups[ i ] = ToolbarModifier.parseGroupToConfigValue( currentGroup );
136 }
137
138 }
139
140 return copy;
141 };
142
143 /**
144 * @param {Function} callback
145 * @param {String} [config]
146 * @param {Boolean} [forceKeepRemoveButtons=false]
147 * @private
148 */
149 ToolbarModifier.prototype._onInit = function( callback, config, forceKeepRemoveButtons ) {
150 forceKeepRemoveButtons = ( forceKeepRemoveButtons === true );
151 AbstractToolbarModifier.prototype._onInit.call( this, undefined, config );
152
153 this.removedButtons = [];
154
155 if ( forceKeepRemoveButtons ) {
156 if ( this.actualConfig.removeButtons ) {
157 this.removedButtons = this.actualConfig.removeButtons.split( ',' );
158 } else {
159 this.removedButtons = [];
160 }
161 } else {
162 if ( !( 'removeButtons' in this.originalConfig ) ) {
163 this.originalConfig.removeButtons = '';
164 this.removedButtons = [];
165 } else {
166 this.removedButtons = this.originalConfig.removeButtons ? this.originalConfig.removeButtons.split( ',' ) : [];
167 }
168 }
169
170 if ( !this.actualConfig.toolbarGroups )
171 this.actualConfig.toolbarGroups = this.fullToolbarEditor.getFullToolbarGroupsConfig();
172
173 this._fixGroups( this.actualConfig );
174 this._calculateTotalBtns();
175
176 this._createModifier();
177 this._refreshMoveBtnsAvalibility();
178 this._refreshBtnTabIndexes();
179
180 if ( typeof callback === 'function' )
181 callback( this.mainContainer );
182 };
183
184 /**
185 * @private
186 */
187 ToolbarModifier.prototype._showConfigurationTool = function() {
188 this.configContainer.addClass( 'hidden' );
189 this.modifyContainer.removeClass( 'hidden' );
190 };
191
192 /**
193 * Show configuration file in tool
194 *
195 * @private
196 */
197 ToolbarModifier.prototype._showConfig = function() {
198 var that = this,
199 actualConfig = this.getActualConfig(),
200 cfg = {};
201 if ( actualConfig.toolbarGroups ) {
202 cfg.toolbarGroups = actualConfig.toolbarGroups;
203
204 var groups = prepareGroups( actualConfig.toolbarGroups, this.cfg.trimEmptyGroups );
205
206 cfg.toolbarGroups = '\n\t\t' + groups.join( ',\n\t\t' );
207 }
208
209 function prepareGroups( toolbarGroups, trimEmptyGroups ) {
210 var groups = [],
211 max = toolbarGroups.length;
212
213 for ( var i = 0; i < max; i++ ) {
214 var group = toolbarGroups[ i ];
215
216 if ( group === '/' ) {
217 groups.push( '\'/\'' );
218 continue;
219 }
220
221 if ( trimEmptyGroups ) {
222 var max2 = group.groups.length;
223 while ( max2-- ) {
224 var subgroup = group.groups[ max2 ];
225
226 if ( ToolbarModifier.getTotalSubGroupButtonsNumber( subgroup, that.fullToolbarEditor ) === 0 ) {
227 group.groups.splice( max2, 1 );
228 }
229 }
230 }
231
232 if ( !( trimEmptyGroups && group.groups.length === 0 ) ) {
233 groups.push( AbstractToolbarModifier.stringifyJSONintoOneLine( group, {
234 addSpaces: true,
235 noQuotesOnKey: true,
236 singleQuotes: true
237 } ) );
238 }
239 }
240
241 return groups;
242 }
243
244 if ( actualConfig.removeButtons ) {
245 cfg.removeButtons = actualConfig.removeButtons;
246 }
247
248 var content = [
249 '<textarea class="configCode" readonly>',
250 'CKEDITOR.editorConfig = function( config ) {\n',
251 ( cfg.toolbarGroups ? '\tconfig.toolbarGroups = [' + cfg.toolbarGroups + '\n\t];' : '' ),
252 ( cfg.removeButtons ? '\n\n' : '' ),
253 ( cfg.removeButtons ? '\tconfig.removeButtons = \'' + cfg.removeButtons + '\';' : '' ),
254 '\n};',
255 '</textarea>'
256 ].join( '' );
257
258
259
260 this.modifyContainer.addClass( 'hidden' );
261 this.configContainer.removeClass( 'hidden' );
262
263 this.configContainer.setHtml( content );
264 };
265
266 /**
267 * Toggle empty groups and subgroups visibility.
268 *
269 * @private
270 */
271 ToolbarModifier.prototype._toggleVisibilityEmptyElements = function() {
272 if ( this.modifyContainer.hasClass( 'empty-visible' ) ) {
273 this.modifyContainer.removeClass( 'empty-visible' );
274 this.emptyVisible = false;
275 } else {
276 this.modifyContainer.addClass( 'empty-visible' );
277 this.emptyVisible = true;
278 }
279
280 this._refreshMoveBtnsAvalibility();
281 };
282
283 /**
284 * Creates HTML main container of modifier.
285 *
286 * @returns {CKEDITOR.dom.element}
287 * @private
288 */
289 ToolbarModifier.prototype._createModifier = function() {
290 var that = this;
291
292 AbstractToolbarModifier.prototype._createModifier.call( this );
293
294 this.modifyContainer.setHtml( this._toolbarConfigToListString() );
295
296 var groupLi = this.modifyContainer.find( 'li[data-type="group"]' );
297
298 this.modifyContainer.on( 'mouseleave', function() {
299 this._dehighlightActiveToolGroup();
300 }, this );
301
302 var max = groupLi.count();
303 for ( var i = 0; i < max; i += 1 ) {
304 groupLi.getItem( i ).on( 'mouseenter', onGroupHover );
305 }
306
307 function onGroupHover() {
308 that._highlightGroup( this.data( 'name' ) );
309 }
310
311 CKEDITOR.document.on( 'keypress', function( e ) {
312 var nativeEvent = e.data.$,
313 keyCode = nativeEvent.keyCode,
314 spaceOrEnter = ( keyCode === 32 || keyCode === 13 ),
315 active = new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement );
316
317 var mainContainer = active.getAscendant( function( node ) {
318 return node.$ === that.mainContainer.$;
319 } );
320
321 if ( !mainContainer || !spaceOrEnter ) {
322 return;
323 }
324
325 if ( active.data( 'type' ) === 'button' ) {
326 active.findOne( 'input' ).$.click();
327 }
328 } );
329
330 this.modifyContainer.on( 'click', function( e ) {
331 var origEvent = e.data.$,
332 target = new CKEDITOR.dom.element( ( origEvent.target || origEvent.srcElement ) ),
333 relativeGroupOrSeparatorLi = ToolbarModifier.getGroupOrSeparatorLiAncestor( target );
334
335 if ( !relativeGroupOrSeparatorLi ) {
336 return;
337 }
338
339 that.cachedActiveElement = document.activeElement;
340
341 // checkbox clicked
342 if ( target.$ instanceof HTMLInputElement )
343 that._handleCheckboxClicked( target );
344
345 // link clicked
346 else if ( target.$ instanceof HTMLButtonElement ) {
347 if ( origEvent.preventDefault )
348 origEvent.preventDefault();
349 else
350 origEvent.returnValue = false;
351
352 var result = that._handleAnchorClicked( target.$ );
353
354 if ( result && result.action == 'remove' )
355 return;
356
357 }
358
359 var elementType = relativeGroupOrSeparatorLi.data( 'type' ),
360 elementName = relativeGroupOrSeparatorLi.data( 'name' );
361
362 that._setActiveElement( elementType, elementName );
363
364 if ( that.cachedActiveElement )
365 that.cachedActiveElement.focus();
366 } );
367
368 if ( !this.toolbarContainer ) {
369 this._createToolbar();
370 this.toolbarContainer.insertBefore( this.mainContainer.getChildren().getItem( 0 ) );
371 }
372
373 this.showToolbarBtnsByGroupName( 'edit' );
374
375 if ( !this.configContainer ) {
376 this.configContainer = new CKEDITOR.dom.element( 'div' );
377 this.configContainer.addClass( 'configContainer' );
378 this.configContainer.addClass( 'hidden' );
379
380 this.mainContainer.append( this.configContainer );
381 }
382
383 return this.mainContainer;
384 };
385
386 /**
387 * Show toolbar buttons related to group name provided in argument
388 * and hide other buttons
389 * Please note: this method works on toolbar in tool, which is located
390 * on top of the tool
391 *
392 * @param {String} groupName
393 */
394 ToolbarModifier.prototype.showToolbarBtnsByGroupName = function( groupName ) {
395 if ( !this.toolbarContainer ) {
396 return;
397 }
398
399 var allButtons = this.toolbarContainer.find( 'button' );
400
401 var max = allButtons.count();
402 for ( var i = 0; i < max; i += 1 ) {
403 var currentBtn = allButtons.getItem( i );
404
405 if ( currentBtn.data( 'group' ) == groupName )
406 currentBtn.removeClass( 'hidden' );
407 else
408 currentBtn.addClass( 'hidden' );
409
410 }
411 };
412
413 /**
414 * Parse group "model" to configuration value
415 *
416 * @param {Object} group
417 * @returns {Object}
418 * @private
419 */
420 ToolbarModifier.parseGroupToConfigValue = function( group ) {
421 if ( group.type == 'separator' ) {
422 return '/';
423 }
424
425 var groups = group.groups,
426 max = groups.length;
427
428 delete group.totalBtns;
429 for ( var i = 0; i < max; i += 1 ) {
430 groups[ i ] = groups[ i ].name;
431 }
432
433 return group;
434 };
435
436 /**
437 * Find closest Li ancestor in DOM tree which is group or separator element
438 *
439 * @param {CKEDITOR.dom.element} element
440 * @returns {CKEDITOR.dom.element}
441 */
442 ToolbarModifier.getGroupOrSeparatorLiAncestor = function( element ) {
443 if ( element.$ instanceof HTMLLIElement && element.data( 'type' ) == 'group' )
444 return element;
445 else {
446 return ToolbarModifier.getFirstAncestor( element, function( ancestor ) {
447 var type = ancestor.data( 'type' );
448
449 return ( type == 'group' || type == 'separator' );
450 } );
451 }
452 };
453
454 /**
455 * Set active element in tool by provided type and name.
456 *
457 * @param {String} type
458 * @param {String} name
459 */
460 ToolbarModifier.prototype._setActiveElement = function( type, name ) {
461 // clear current active element
462 if ( this.currentActive )
463 this.currentActive.elem.removeClass( 'active' );
464
465 if ( type === null ) {
466 this._dehighlightActiveToolGroup();
467 this.currentActive = null;
468 return;
469 }
470
471 var liElem = this.mainContainer.findOne( 'ul[data-type=table-body] li[data-type="' + type + '"][data-name="' + name + '"]' );
472
473 liElem.addClass( 'active' );
474
475 // setup model
476 this.currentActive = {
477 type: type,
478 name: name,
479 elem: liElem
480 };
481
482 // highlight group in toolbar
483 if ( type == 'group' )
484 this._highlightGroup( name );
485
486 if ( type == 'separator' )
487 this._dehighlightActiveToolGroup();
488 };
489
490 /**
491 * @returns {CKEDITOR.dom.element|null}
492 */
493 ToolbarModifier.prototype.getActiveToolGroup = function() {
494 if ( this.editorInstance.container )
495 return this.editorInstance.container.findOne( '.cke_toolgroup.active, .cke_toolbar.active' );
496 else
497 return null;
498 };
499
500 /**
501 * @private
502 */
503 ToolbarModifier.prototype._dehighlightActiveToolGroup = function() {
504 var currentActive = this.getActiveToolGroup();
505
506 if ( currentActive )
507 currentActive.removeClass( 'active' );
508
509 // @see ToolbarModifier.prototype._highlightGroup.
510 if ( this.editorInstance.container ) {
511 this.editorInstance.container.removeClass( 'some-toolbar-active' );
512 }
513 };
514
515 /**
516 * Highlight group by its name, and dehighlight current group.
517 *
518 * @param {String} name
519 */
520 ToolbarModifier.prototype._highlightGroup = function( name ) {
521 if ( !this.editorInstance.container )
522 return;
523
524 var foundBtnName = this.getFirstEnabledButtonInGroup( name ),
525 foundBtn = this.editorInstance.container.findOne( '.cke_button__' + foundBtnName + ', .cke_combo__' + foundBtnName );
526
527 this._dehighlightActiveToolGroup();
528
529 // Helpful to dim other toolbar groups if one is highlighted.
530 if ( this.editorInstance.container ) {
531 this.editorInstance.container.addClass( 'some-toolbar-active' );
532 }
533
534 if ( foundBtn ) {
535 var btnToolbar = ToolbarModifier.getFirstAncestor( foundBtn, function( ancestor ) {
536 return ancestor.hasClass( 'cke_toolbar' );
537 } );
538
539 if ( btnToolbar )
540 btnToolbar.addClass( 'active' );
541 }
542 };
543
544 /**
545 * @param {String} groupName
546 * @return {String|null}
547 */
548 ToolbarModifier.prototype.getFirstEnabledButtonInGroup = function( groupName ) {
549 var groups = this.actualConfig.toolbarGroups,
550 groupIndex = this.getGroupIndex( groupName ),
551 group = groups[ groupIndex ];
552
553 if ( groupIndex === -1 ) {
554 return null;
555 }
556
557 var max = group.groups ? group.groups.length : 0;
558 for ( var i = 0; i < max; i += 1 ) {
559 var currSubgroupName = group.groups[ i ].name,
560 firstEnabled = this.getFirstEnabledButtonInSubgroup( currSubgroupName );
561
562 if ( firstEnabled )
563 return firstEnabled;
564 }
565 return null;
566 };
567
568 /**
569 * @param {String} subgroupName
570 * @returns {String|null}
571 */
572 ToolbarModifier.prototype.getFirstEnabledButtonInSubgroup = function( subgroupName ) {
573 var subgroupBtns = this.fullToolbarEditor.buttonsByGroup[ subgroupName ];
574
575 var max = subgroupBtns ? subgroupBtns.length : 0;
576 for ( var i = 0; i < max; i += 1 ) {
577 var currBtnName = subgroupBtns[ i ].name;
578 if ( !this.isButtonRemoved( currBtnName ) )
579 return currBtnName;
580 }
581
582 return null;
583 };
584
585 /**
586 * Sets up parameters and call adequate action.
587 *
588 * @param {CKEDITOR.dom.element} checkbox
589 * @private
590 */
591 ToolbarModifier.prototype._handleCheckboxClicked = function( checkbox ) {
592 var closestLi = checkbox.getAscendant( 'li' ),
593 elementName = closestLi.data( 'name' ),
594 aboutToAddToRemoved = !checkbox.$.checked;
595
596 if ( aboutToAddToRemoved )
597 this._addButtonToRemoved( elementName );
598 else
599 this._removeButtonFromRemoved( elementName );
600 };
601
602 /**
603 * Sets up parameters and call adequate action.
604 *
605 * @param {HTMLAnchorElement} anchor
606 * @private
607 */
608 ToolbarModifier.prototype._handleAnchorClicked = function( anchor ) {
609 var anchorDOM = new CKEDITOR.dom.element( anchor ),
610 relativeLi = anchorDOM.getAscendant( 'li' ),
611 relativeUl = relativeLi.getAscendant( 'ul' ),
612 elementType = relativeLi.data( 'type' ),
613 elementName = relativeLi.data( 'name' ),
614 direction = anchorDOM.data( 'direction' ),
615 nearestLi = ( direction === 'up' ? relativeLi.getPrevious() : relativeLi.getNext() ),
616 groupName,
617 subgroupName,
618 newIndex;
619
620 // nothing to do
621 if ( anchorDOM.hasClass( 'disabled' ) )
622 return null;
623
624 // remove separator and nothing else
625 if ( anchorDOM.hasClass( 'remove' ) ) {
626 relativeLi.remove();
627 this._removeSeparator( relativeLi.data( 'name' ) );
628 this._setActiveElement( null );
629 return { action: 'remove' };
630 }
631
632 if ( !anchorDOM.hasClass( 'move' ) || !nearestLi )
633 return { action: null };
634
635 // move group or separator
636 if ( elementType === 'group' || elementType === 'separator' ) {
637 groupName = elementName;
638 newIndex = this._moveGroup( direction, groupName );
639 }
640
641 // move subgroup
642 if ( elementType === 'subgroup' ) {
643 subgroupName = elementName;
644 groupName = relativeLi.getAscendant( 'li' ).data( 'name' );
645 newIndex = this._moveSubgroup( direction, groupName, subgroupName );
646 }
647
648 // Visual effect
649 if ( direction === 'up' )
650 relativeLi.insertBefore( relativeUl.getChild( newIndex ) );
651
652 if ( direction === 'down' )
653 relativeLi.insertAfter( relativeUl.getChild( newIndex ) );
654
655 // Should know whether there is next li element after modifications.
656 var nextLi = relativeLi;
657
658 // We are looking for next li element in list (to check whether current one is the last one)
659 var found;
660 while ( nextLi = ( direction === 'up' ? nextLi.getPrevious() : nextLi.getNext() ) ) {
661 if ( !this.emptyVisible && nextLi.hasClass( 'empty' ) ) {
662 continue;
663 }
664
665 found = nextLi;
666 break;
667 }
668
669 // If not found, it means that we reached end.
670 if ( !found ) {
671 var selector = ( '[data-direction="' + ( direction === 'up' ? 'down' : 'up' ) + '"]' );
672
673 // Shifting direction.
674 this.cachedActiveElement = anchorDOM.getParent().findOne( selector );
675 }
676
677 this._refreshMoveBtnsAvalibility();
678 this._refreshBtnTabIndexes();
679
680 return {
681 action: 'move'
682 };
683 };
684
685 /**
686 * First element can not be moved up, and last element can not be moved down,
687 * so they are disabled.
688 */
689 ToolbarModifier.prototype._refreshMoveBtnsAvalibility = function() {
690 var that = this,
691 disabledBtns = this.mainContainer.find( 'ul[data-type=table-body] li > p > span > button.move.disabled' );
692
693 // enabling all disabled buttons
694 var max = disabledBtns.count();
695 for ( var i = 0; i < max; i += 1 ) {
696 var currentBtn = disabledBtns.getItem( i );
697 currentBtn.removeClass( 'disabled' );
698 }
699
700
701 function disableElementsInLists( ulList ) {
702 var max = ulList.count();
703 for ( i = 0; i < max; i += 1 ) {
704 that._disableElementsInList( ulList.getItem( i ) );
705 }
706 }
707
708 // Disable buttons in toolbars.
709 disableElementsInLists( this.mainContainer.find( 'ul[data-type=table-body]' ) );
710
711 // Disable buttons in toolbar groups.
712 disableElementsInLists( this.mainContainer.find( 'ul[data-type=table-body] > li > ul' ) );
713 };
714
715 /**
716 * @private
717 */
718 ToolbarModifier.prototype._refreshBtnTabIndexes = function() {
719 var tabindexed = this.mainContainer.find( '[data-tab="true"]' );
720
721 var max = tabindexed.count();
722 for ( var i = 0; i < max; i++ ) {
723 var item = tabindexed.getItem( i ),
724 disabled = item.hasClass( 'disabled' );
725
726 item.setAttribute( 'tabindex', disabled ? -1 : i );
727 }
728 };
729
730 /**
731 * Disable buttons to move elements up and down which should be disabled.
732 *
733 * @param {CKEDITOR.dom.element} ul
734 * @private
735 */
736 ToolbarModifier.prototype._disableElementsInList = function( ul ) {
737 var liList = ul.getChildren();
738
739 if ( !liList.count() )
740 return;
741
742 var firstDisabled, lastDisabled;
743 if ( this.emptyVisible ) {
744 firstDisabled = ul.getFirst();
745 lastDisabled = ul.getLast();
746 } else {
747 firstDisabled = ul.getFirst( isNotEmptyChecker );
748 lastDisabled = ul.getLast( isNotEmptyChecker );
749 }
750
751 function isNotEmptyChecker( element ) {
752 return !element.hasClass( 'empty' );
753 }
754
755 if ( firstDisabled )
756 var firstDisabledBtn = firstDisabled.findOne( 'p button[data-direction="up"]' );
757
758 if ( lastDisabled )
759 var lastDisabledBtn = lastDisabled.findOne( 'p button[data-direction="down"]' );
760
761 if ( firstDisabledBtn ) {
762 firstDisabledBtn.addClass( 'disabled' );
763 firstDisabledBtn.setAttribute( 'tabindex', '-1' );
764 }
765
766 if ( lastDisabledBtn ) {
767 lastDisabledBtn.addClass( 'disabled' );
768 lastDisabledBtn.setAttribute( 'tabindex', '-1' );
769 }
770 };
771
772 /**
773 * Gets group index in actual config toolbarGroups
774 *
775 * @param {String} name
776 * @returns {Number}
777 */
778 ToolbarModifier.prototype.getGroupIndex = function( name ) {
779 var groups = this.actualConfig.toolbarGroups;
780
781 var max = groups.length;
782 for ( var i = 0; i < max; i += 1 ) {
783 if ( groups[ i ].name === name )
784 return i;
785 }
786
787 return -1;
788 };
789
790 /**
791 * Handle adding separator.
792 *
793 * @private
794 */
795 ToolbarModifier.prototype._addSeparator = function() {
796 var separatorIndex = this._determineSeparatorToAddIndex(),
797 separator = ToolbarModifier.createSeparatorLiteral(),
798 domSeparator = CKEDITOR.dom.element.createFromHtml( ToolbarModifier.getToolbarSeparatorString( separator ) );
799
800 this.actualConfig.toolbarGroups.splice( separatorIndex, 0, separator );
801
802 domSeparator.insertBefore( this.modifyContainer.findOne( 'ul[data-type=table-body]' ).getChild( separatorIndex ) );
803
804 this._setActiveElement( 'separator', separator.name );
805 this._refreshMoveBtnsAvalibility();
806 this._refreshBtnTabIndexes();
807 this._refreshEditor();
808 };
809
810 /**
811 * Handle removing separator.
812 *
813 * @param {String} name
814 */
815 ToolbarModifier.prototype._removeSeparator = function( name ) {
816 var separatorIndex = CKEDITOR.tools.indexOf( this.actualConfig.toolbarGroups, function( group ) {
817 return group.type == 'separator' && group.name == name;
818 } );
819
820 this.actualConfig.toolbarGroups.splice( separatorIndex, 1 );
821
822 this._refreshMoveBtnsAvalibility();
823 this._refreshBtnTabIndexes();
824 this._refreshEditor();
825 };
826
827 /**
828 * Determine index where separator should be added, based on currently selected element.
829 *
830 * @returns {Number}
831 * @private
832 */
833 ToolbarModifier.prototype._determineSeparatorToAddIndex = function() {
834 if ( !this.currentActive )
835 return 0;
836
837 var groupLi;
838 if ( this.currentActive.elem.data( 'type' ) == 'group' || this.currentActive.elem.data( 'type' ) == 'separator' )
839 groupLi = this.currentActive.elem;
840 else
841 groupLi = this.currentActive.elem.getAscendant( 'li' );
842
843 return groupLi.getIndex();
844 };
845
846 /**
847 * @param {Array} elementsArray
848 * @param {Number} elementIndex
849 * @param {String} direction
850 * @returns {Number}
851 * @private
852 */
853 ToolbarModifier.prototype._moveElement = function( elementsArray, elementIndex, direction ) {
854 var nextIndex;
855
856 if ( this.emptyVisible )
857 nextIndex = ( direction == 'down' ? elementIndex + 1 : elementIndex - 1 );
858 else {
859 // When empty elements are not visible, there is need to skip them.
860 nextIndex = ToolbarModifier.getFirstElementIndexWith( elementsArray, elementIndex, direction, isEmptyOrSeparatorChecker );
861 }
862
863 function isEmptyOrSeparatorChecker( element ) {
864 return element.totalBtns || element.type == 'separator';
865 }
866
867 var offset = nextIndex - elementIndex;
868
869 return ToolbarModifier.moveTo( offset, elementsArray, elementIndex );
870 };
871
872 /**
873 * Moves group located in config level up or down and refresh editor.
874 *
875 * @param {String} direction
876 * @param {String} groupName
877 * @returns {Number}
878 */
879 ToolbarModifier.prototype._moveGroup = function( direction, groupName ) {
880 var groupIndex = this.getGroupIndex( groupName ),
881 groups = this.actualConfig.toolbarGroups,
882 newIndex = this._moveElement( groups, groupIndex, direction );
883
884 this._refreshMoveBtnsAvalibility();
885 this._refreshBtnTabIndexes();
886 this._refreshEditor();
887
888 return newIndex;
889 };
890
891 /**
892 * Moves subgroup located in config level up or down and refresh editor.
893 *
894 * @param {String} direction
895 * @param {String} groupName
896 * @param {String} subgroupName
897 * @private
898 */
899 ToolbarModifier.prototype._moveSubgroup = function( direction, groupName, subgroupName ) {
900 var groupIndex = this.getGroupIndex( groupName ),
901 groups = this.actualConfig.toolbarGroups,
902 group = groups[ groupIndex ],
903 subgroupIndex = CKEDITOR.tools.indexOf( group.groups, function( subgroup ) {
904 return subgroup.name == subgroupName;
905 } ),
906 newIndex = this._moveElement( group.groups, subgroupIndex, direction );
907
908 this._refreshEditor();
909
910 return newIndex;
911 };
912
913 /**
914 * Set `totalBtns` property in `actualConfig.toolbarGroups` elements.
915 *
916 * @private
917 */
918 ToolbarModifier.prototype._calculateTotalBtns = function() {
919 var groups = this.actualConfig.toolbarGroups;
920
921 var i = groups.length;
922 // from the end
923 while ( i-- ) {
924 var currentGroup = groups[ i ],
925 totalBtns = ToolbarModifier.getTotalGroupButtonsNumber( currentGroup, this.fullToolbarEditor );
926
927 if ( currentGroup.type == 'separator' ) {
928 // nothing to do with separator
929 continue;
930 }
931
932 currentGroup.totalBtns = totalBtns;
933 }
934 };
935
936 /**
937 * Add button to removeButtons field in config and refresh editor.
938 *
939 * @param {String} buttonName
940 * @private
941 */
942 ToolbarModifier.prototype._addButtonToRemoved = function( buttonName ) {
943 if ( CKEDITOR.tools.indexOf( this.removedButtons, buttonName ) != -1 )
944 throw 'Button already added to removed';
945
946 this.removedButtons.push( buttonName );
947 this.actualConfig.removeButtons = this.removedButtons.join( ',' );
948 this._refreshEditor();
949 };
950
951 /**
952 * Remove button from removeButtons field in config and refresh editor.
953 *
954 * @param {String} buttonName
955 * @private
956 */
957 ToolbarModifier.prototype._removeButtonFromRemoved = function( buttonName ) {
958 var foundAtIndex = CKEDITOR.tools.indexOf( this.removedButtons, buttonName );
959
960 if ( foundAtIndex === -1 )
961 throw 'Trying to remove button from removed, but not found';
962
963 this.removedButtons.splice( foundAtIndex, 1 );
964 this.actualConfig.removeButtons = this.removedButtons.join( ',' );
965 this._refreshEditor();
966 };
967
968 /**
969 * Parse group "model" to configuration value
970 *
971 * @param {Object} group
972 * @returns {Object}
973 * @static
974 */
975 ToolbarModifier.parseGroupToConfigValue = function( group ) {
976 if ( group.type == 'separator' ) {
977 return '/';
978 }
979
980 var groups = group.groups,
981 max = groups.length;
982
983 delete group.totalBtns;
984 for ( var i = 0; i < max; i += 1 ) {
985 groups[ i ] = groups[ i ].name;
986 }
987
988 return group;
989 };
990
991 /**
992 * Find closest Li ancestor in DOM tree which is group or separator element
993 *
994 * @param {CKEDITOR.dom.element} element
995 * @returns {CKEDITOR.dom.element}
996 * @static
997 */
998 ToolbarModifier.getGroupOrSeparatorLiAncestor = function( element ) {
999 if ( element.$ instanceof HTMLLIElement && element.data( 'type' ) == 'group' )
1000 return element;
1001 else {
1002 return ToolbarModifier.getFirstAncestor( element, function( ancestor ) {
1003 var type = ancestor.data( 'type' );
1004
1005 return ( type == 'group' || type == 'separator' );
1006 } );
1007 }
1008 };
1009
1010 /**
1011 * Create separator literal with unique id.
1012 *
1013 * @public
1014 * @static
1015 * @return {Object}
1016 */
1017 ToolbarModifier.createSeparatorLiteral = function() {
1018 return {
1019 type: 'separator',
1020 name: ( 'separator' + CKEDITOR.tools.getNextNumber() )
1021 };
1022 };
1023
1024 /**
1025 * Creates HTML unordered list string based on toolbarGroups field in config.
1026 *
1027 * @returns {String}
1028 * @static
1029 */
1030 ToolbarModifier.prototype._toolbarConfigToListString = function() {
1031 var groups = this.actualConfig.toolbarGroups || [],
1032 listString = '<ul data-type="table-body">';
1033
1034 var max = groups.length;
1035 for ( var i = 0; i < max; i += 1 ) {
1036 var currentGroup = groups[ i ];
1037
1038 if ( currentGroup.type === 'separator' )
1039 listString += ToolbarModifier.getToolbarSeparatorString( currentGroup );
1040 else
1041 listString += this._getToolbarGroupString( currentGroup );
1042 }
1043
1044 listString += '</ul>';
1045
1046 var headerString = ToolbarModifier.getToolbarHeaderString();
1047
1048 return headerString + listString;
1049 };
1050
1051 /**
1052 * Created HTML group list element based on group field in config.
1053 *
1054 * @param {Object} group
1055 * @returns {String}
1056 * @private
1057 */
1058 ToolbarModifier.prototype._getToolbarGroupString = function( group ) {
1059 var subgroups = group.groups,
1060 groupString = '';
1061
1062 groupString += [
1063 '<li ',
1064 'data-type="group" ',
1065 'data-name="', group.name, '" ',
1066 ( group.totalBtns ? '' : 'class="empty"' ),
1067 '>'
1068 ].join( '' );
1069 groupString += ToolbarModifier.getToolbarElementPreString( group ) + '<ul>';
1070
1071 var max = subgroups.length;
1072
1073 for ( var i = 0; i < max; i += 1 ) {
1074 var currentSubgroup = subgroups[ i ],
1075 subgroupBtns = this.fullToolbarEditor.buttonsByGroup[ currentSubgroup.name ];
1076
1077 groupString += this._getToolbarSubgroupString( currentSubgroup, subgroupBtns );
1078 }
1079 groupString += '</ul></li>';
1080
1081 return groupString;
1082 };
1083
1084 /**
1085 * @param {Object} separator
1086 * @returns {String}
1087 * @static
1088 */
1089 ToolbarModifier.getToolbarSeparatorString = function( separator ) {
1090 return [
1091 '<li ',
1092 'data-type="', separator.type , '" ',
1093 'data-name="', separator.name , '"',
1094 '>',
1095 ToolbarModifier.getToolbarElementPreString( 'row separator' ),
1096 '</li>'
1097 ].join( '' );
1098 };
1099
1100 /**
1101 * @returns {string}
1102 */
1103 ToolbarModifier.getToolbarHeaderString = function() {
1104 return '<ul data-type="table-header">' +
1105 '<li data-type="header">' +
1106 '<p>Toolbars</p>' +
1107 '<ul>' +
1108 '<li>' +
1109 '<p>Toolbar groups</p>' +
1110 '<p>Toolbar group items</p>' +
1111 '</li>' +
1112 '</ul>' +
1113 '</li>' +
1114 '</ul>';
1115 };
1116
1117 /**
1118 * Find and return first ancestor of element provided in first argument
1119 * which match the criteria checked in function provided in second argument.
1120 *
1121 * @param {CKEDITOR.dom.element} element
1122 * @param {Function} checker
1123 * @returns {CKEDITOR.dom.element|null}
1124 */
1125 ToolbarModifier.getFirstAncestor = function( element, checker ) {
1126 var ancestors = element.getParents(),
1127 i = ancestors.length;
1128
1129 while ( i-- ) {
1130 if ( checker( ancestors[ i ] ) )
1131 return ancestors[ i ];
1132 }
1133
1134 return null;
1135 };
1136
1137 /**
1138 * Looking through array elements start from index provided in second argument
1139 * and go 'up' or 'down' in array
1140 * last argument is condition checker which should return Boolean value
1141 *
1142 * User cases:
1143 *
1144 * ToolbarModifier.getFirstElementIndexWith( [3, 4, 8, 1, 4], 2, 'down', function( elem ) { return elem == 4; } ); // 4
1145 * ToolbarModifier.getFirstElementIndexWith( [3, 4, 8, 1, 4], 2, 'up', function( elem ) { return elem == 4; } ); // 1
1146 *
1147 * @param {Array} array
1148 * @param {Number} i
1149 * @param {String} direction 'up' or 'down'
1150 * @param {Function} conditionChecker
1151 * @static
1152 * @returns {Number} index of found element
1153 */
1154 ToolbarModifier.getFirstElementIndexWith = function( array, i, direction, conditionChecker ) {
1155 function whileChecker() {
1156 var result;
1157 if ( direction === 'up' )
1158 result = i--;
1159 else
1160 result = ( ++i < array.length );
1161
1162 return result;
1163 }
1164
1165 while ( whileChecker() ) {
1166 if ( conditionChecker( array[ i ] ) )
1167 return i;
1168
1169 }
1170
1171 return -1;
1172 };
1173
1174 /**
1175 * Moves array element at index level up or down.
1176 *
1177 * @static
1178 * @param {String} direction
1179 * @param {Array} array
1180 * @param {Number} index
1181 * @returns {Number}
1182 */
1183 ToolbarModifier.moveTo = function( offset, array, index ) {
1184 var element, newIndex;
1185
1186 if ( index !== -1 )
1187 element = array.splice( index, 1 )[ 0 ];
1188
1189 newIndex = index + offset;
1190
1191 array.splice( newIndex, 0, element );
1192
1193 return newIndex;
1194 };
1195
1196 /**
1197 * @static
1198 * @param {Object} subgroup
1199 * @returns {Number}
1200 */
1201 ToolbarModifier.getTotalSubGroupButtonsNumber = function( subgroup, fullToolbarEditor ) {
1202 var subgroupName = ( typeof subgroup == 'string' ? subgroup : subgroup.name ),
1203 subgroupBtns = fullToolbarEditor.buttonsByGroup[ subgroupName ];
1204
1205 return ( subgroupBtns ? subgroupBtns.length : 0 );
1206 };
1207
1208 /**
1209 * Returns all buttons number in group which are nested in subgroups also.
1210 *
1211 * @param {Object} group
1212 * @param {ToolbarModifier.FullToolbarEditor}
1213 * @static
1214 * @returns {Number}
1215 */
1216 ToolbarModifier.getTotalGroupButtonsNumber = function( group, fullToolbarEditor ) {
1217 var total = 0,
1218 subgroups = group.groups;
1219
1220 var max = subgroups ? subgroups.length : 0;
1221 for ( var i = 0; i < max; i += 1 )
1222 total += ToolbarModifier.getTotalSubGroupButtonsNumber( subgroups[ i ], fullToolbarEditor );
1223
1224 return total;
1225 };
1226
1227 /**
1228 * Creates HTML subgroup list element based on subgroup field in config.
1229 *
1230 * @param {Object} subgroup
1231 * @param {Array} groupBtns
1232 * @returns {String}
1233 * @private
1234 */
1235 ToolbarModifier.prototype._getToolbarSubgroupString = function( subgroup, groupBtns ) {
1236 var subgroupString = '';
1237
1238 subgroupString += [
1239 '<li ',
1240 'data-type="subgroup" ',
1241 'data-name="', subgroup.name, '" ',
1242 ( subgroup.totalBtns ? '' : 'class="empty" ' ),
1243 '>'
1244 ].join( '' );
1245 subgroupString += ToolbarModifier.getToolbarElementPreString( subgroup.name );
1246 subgroupString += '<ul>';
1247
1248 var max = groupBtns ? groupBtns.length : 0;
1249 for ( var i = 0; i < max; i += 1 )
1250 subgroupString += this.getButtonString( groupBtns[ i ] );
1251
1252 subgroupString += '</ul>';
1253
1254 subgroupString += '</li>';
1255
1256 return subgroupString;
1257 };
1258
1259 /**
1260 * @param {String} buttonName
1261 * @returns {String|null}
1262 * @private
1263 */
1264 ToolbarModifier.prototype._getConfigButtonName = function( buttonName ) {
1265 var items = this.fullToolbarEditor.editorInstance.ui.items;
1266
1267 var name;
1268 for ( name in items ) {
1269 if ( items[ name ].name == buttonName )
1270 return name;
1271 }
1272
1273 return null;
1274 };
1275
1276 /**
1277 * @param {String} buttonName
1278 * @returns {Boolean}
1279 */
1280 ToolbarModifier.prototype.isButtonRemoved = function( buttonName ) {
1281 return CKEDITOR.tools.indexOf( this.removedButtons, this._getConfigButtonName( buttonName ) ) != -1;
1282 };
1283
1284 /**
1285 * @param {CKEDITOR.ui.button/CKEDITOR.ui.richCombo} button
1286 * @returns {String}
1287 * @public
1288 */
1289 ToolbarModifier.prototype.getButtonString = function( button ) {
1290 var checked = ( this.isButtonRemoved( button.name ) ? '' : 'checked="checked"' );
1291
1292 return [
1293 '<li data-tab="true" data-type="button" data-name="', this._getConfigButtonName( button.name ), '">',
1294 '<label title="', button.label, '" >',
1295 '<input ',
1296 'tabindex="-1"',
1297 'type="checkbox"',
1298 checked,
1299 '/>',
1300 button.$.getOuterHtml(),
1301 '</label>',
1302 '</li>'
1303 ].join( '' );
1304 };
1305
1306 /**
1307 * Creates group header string.
1308 *
1309 * @param {Object|String} group
1310 * @returns {String}
1311 * @static
1312 */
1313 ToolbarModifier.getToolbarElementPreString = function( group ) {
1314 var name = ( group.name ? group.name : group );
1315
1316 return [
1317 '<p>',
1318 '<span>',
1319 '<button title="Move element upward" data-tab="true" data-direction="up" class="move icon-up-big"></button>',
1320 '<button title="Move element downward" data-tab="true" data-direction="down" class="move icon-down-big"></button>',
1321 ( name == 'row separator' ? '<button title="Remove element" data-tab="true" class="remove icon-trash"></button>' : '' ),
1322 name,
1323 '</span>',
1324 '</p>'
1325 ].join( '' );
1326 };
1327
1328 /**
1329 * @static
1330 * @param {String} cfg
1331 * @returns {String}
1332 */
1333 ToolbarModifier.evaluateToolbarGroupsConfig = function( cfg ) {
1334 cfg = ( function( cfg ) {
1335 var config = {}, result;
1336
1337 /*jshint -W002 */
1338 try {
1339 result = eval( '(' + cfg + ')' );
1340 } catch ( e ) {
1341 try {
1342 result = eval( cfg );
1343 } catch ( e ) {
1344 return null;
1345 }
1346 }
1347 /*jshint +W002 */
1348
1349 if ( config.toolbarGroups && typeof config.toolbarGroups.length === 'number' ) {
1350 return JSON.stringify( config );
1351 } else if ( result && typeof result.length === 'number' ) {
1352 return JSON.stringify( { toolbarGroups: result } );
1353 } else if ( result && result.toolbarGroups ) {
1354 return JSON.stringify( result );
1355 } else {
1356 return null;
1357 }
1358
1359 }( cfg ) );
1360
1361 return cfg;
1362 };
1363
1364 return ToolbarModifier;
1365} )();
1366
diff --git a/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js b/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js
new file mode 100644
index 00000000..4c14dd28
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/js/toolbartextmodifier.js
@@ -0,0 +1,623 @@
1/* global CodeMirror, ToolbarConfigurator */
2
3'use strict';
4
5( function() {
6 var AbstractToolbarModifier = ToolbarConfigurator.AbstractToolbarModifier,
7 FullToolbarEditor = ToolbarConfigurator.FullToolbarEditor;
8
9 /**
10 * @class ToolbarConfigurator.ToolbarTextModifier
11 * @param {String} editorId An id of modified editor
12 * @extends AbstractToolbarModifier
13 * @constructor
14 */
15 function ToolbarTextModifier( editorId ) {
16 AbstractToolbarModifier.call( this, editorId );
17
18 this.codeContainer = null;
19 this.hintContainer = null;
20 }
21
22 // Expose the class.
23 ToolbarConfigurator.ToolbarTextModifier = ToolbarTextModifier;
24
25 ToolbarTextModifier.prototype = Object.create( AbstractToolbarModifier.prototype );
26
27 /**
28 * @param {Function} callback
29 * @param {String} [config]
30 * @private
31 */
32 ToolbarTextModifier.prototype._onInit = function( callback, config ) {
33 AbstractToolbarModifier.prototype._onInit.call( this, undefined, config );
34
35 this._createModifier( config ? this.actualConfig : undefined );
36
37 if ( typeof callback === 'function' )
38 callback( this.mainContainer );
39 };
40
41 /**
42 * Creates HTML main container of modifier.
43 *
44 * @param {String} cfg
45 * @returns {CKEDITOR.dom.element}
46 * @private
47 */
48 ToolbarTextModifier.prototype._createModifier = function( cfg ) {
49 var that = this;
50
51 this._createToolbar();
52
53 if ( this.toolbarContainer ) {
54 this.mainContainer.append( this.toolbarContainer );
55 }
56
57 AbstractToolbarModifier.prototype._createModifier.call( this );
58
59 this._setupActualConfig( cfg );
60
61 var toolbarCfg = this.actualConfig.toolbar,
62 cfgValue;
63
64 if ( CKEDITOR.tools.isArray( toolbarCfg ) ) {
65 var stringifiedToolbar = '[\n\t\t' + FullToolbarEditor.map( toolbarCfg, function( json ) {
66 return AbstractToolbarModifier.stringifyJSONintoOneLine( json, {
67 addSpaces: true,
68 noQuotesOnKey: true,
69 singleQuotes: true
70 } );
71 } ).join( ',\n\t\t' ) + '\n\t]';
72
73 cfgValue = '\tconfig.toolbar = ' + stringifiedToolbar + ';';
74 } else {
75 cfgValue = 'config.toolbar = [];';
76 }
77
78 cfgValue = [
79 'CKEDITOR.editorConfig = function( config ) {\n',
80 cfgValue,
81 '\n};'
82 ].join( '' );
83
84 function hint( cm ) {
85 var data = setupData( cm );
86
87 if ( data.charsBetween === null ) {
88 return;
89 }
90
91 var unused = that.getUnusedButtonsArray( that.actualConfig.toolbar, true, data.charsBetween ),
92 to = cm.getCursor(),
93 from = CodeMirror.Pos( to.line, ( to.ch - ( data.charsBetween.length ) ) ),
94 token = cm.getTokenAt( to ),
95 prevToken = cm.getTokenAt( { line: to.line, ch: token.start } );
96
97 // determine that we are at beginning of group,
98 // so first key is "name"
99 if ( prevToken.string === '{' )
100 unused = [ 'name' ];
101
102 // preventing close with special character and move cursor forward
103 // when no autocomplete
104 if ( unused.length === 0 )
105 return;
106
107 return new HintData( from, to, unused );
108 }
109
110 function HintData( from, to, list ) {
111 this.from = from;
112 this.to = to;
113 this.list = list;
114 this._handlers = [];
115 }
116
117 function setupData( cm, character ) {
118 var result = {};
119
120 result.cur = cm.getCursor();
121 result.tok = cm.getTokenAt( result.cur );
122
123 result[ 'char' ] = character || result.tok.string.charAt( result.tok.string.length - 1 );
124
125 // Getting string between begin of line and cursor.
126 var curLineTillCur = cm.getRange( CodeMirror.Pos( result.cur.line, 0 ), result.cur );
127
128 // Reverse string.
129 var currLineTillCurReversed = curLineTillCur.split( '' ).reverse().join( '' );
130
131 // Removing proper string definitions :
132 // FROM:
133 // R' ,'odeR' ,'odnU' [ :smeti{
134 // ^^^^^^ ^^^^^^
135 // TO:
136 // R' , [ :smeti{
137 currLineTillCurReversed = currLineTillCurReversed.replace( /(['|"]\w*['|"])/g, '' );
138
139 // Matching letters till ' or " character and end string char.
140 // R' , [ :smeti{
141 // ^
142 result.charsBetween = currLineTillCurReversed.match( /(^\w*)(['|"])/ );
143
144 if ( result.charsBetween ) {
145 result.endChar = result.charsBetween[ 2 ];
146
147 // And reverse string (bring to original state).
148 result.charsBetween = result.charsBetween[ 1 ].split( '' ).reverse().join( '' );
149 }
150
151 return result;
152 }
153
154 function complete( cm ) {
155 setTimeout( function() {
156 if ( !cm.state.completionActive ) {
157 CodeMirror.showHint( cm, hint, {
158 hintsClass: 'toolbar-modifier',
159 completeSingle: false
160 } );
161 }
162 }, 100 );
163
164 return CodeMirror.Pass;
165 }
166
167 var codeMirrorWrapper = new CKEDITOR.dom.element( 'div' );
168 codeMirrorWrapper.addClass( 'codemirror-wrapper' );
169 this.modifyContainer.append( codeMirrorWrapper );
170 this.codeContainer = CodeMirror( codeMirrorWrapper.$, {
171 mode: { name: 'javascript', json: true },
172 // For some reason (most likely CM's bug) gutter breaks CM's height.
173 // Refreshing CM does not help.
174 lineNumbers: false,
175 lineWrapping: true,
176 // Trick to make CM autogrow. http://codemirror.net/demo/resize.html
177 viewportMargin: Infinity,
178 value: cfgValue,
179 smartIndent: false,
180 indentWithTabs: true,
181 indentUnit: 4,
182 tabSize: 4,
183 theme: 'neo',
184 extraKeys: {
185 'Left': complete,
186 'Right': complete,
187 "'''": complete,
188 "'\"'": complete,
189 Backspace: complete,
190 Delete: complete,
191 'Shift-Tab': 'indentLess'
192 }
193 } );
194
195 this.codeContainer.on( 'endCompletion', function( cm, completionData ) {
196 var data = setupData( cm );
197
198 // preventing close with special character and move cursor forward
199 // when no autocomplete
200 if ( completionData === undefined )
201 return;
202
203 cm.replaceSelection( data.endChar );
204 } );
205
206 this.codeContainer.on( 'change', function() {
207 var value = that.codeContainer.getValue();
208
209 value = that._evaluateValue( value );
210
211 if ( value !== null ) {
212 that.actualConfig.toolbar = ( value.toolbar ? value.toolbar : that.actualConfig.toolbar );
213
214 that._fillHintByUnusedElements();
215 that._refreshEditor();
216
217 that.mainContainer.removeClass( 'invalid' );
218 } else {
219 that.mainContainer.addClass( 'invalid' );
220 }
221 } );
222
223 this.hintContainer = new CKEDITOR.dom.element( 'div' );
224 this.hintContainer.addClass( 'toolbarModifier-hints' );
225
226 this._fillHintByUnusedElements();
227 this.hintContainer.insertBefore( codeMirrorWrapper );
228 };
229
230 /**
231 * Create DOM string and set to hint container,
232 * show proper information when no unused element left.
233 *
234 * @private
235 */
236 ToolbarTextModifier.prototype._fillHintByUnusedElements = function() {
237 var unused = this.getUnusedButtonsArray( this.actualConfig.toolbar, true );
238 unused = this.groupButtonNamesByGroup( unused );
239
240 var unusedElements = FullToolbarEditor.map( unused, function( elem ) {
241 var buttonsList = FullToolbarEditor.map( elem.buttons, function( buttonName ) {
242 return '<code>' + buttonName + '</code> ';
243 } ).join( '' );
244
245 return [
246 '<dt>',
247 '<code>', elem.name, '</code>',
248 '</dt>',
249 '<dd>',
250 buttonsList,
251 '</dd>'
252 ].join( '' );
253 } ).join( ' ' );
254
255 var listHeader = [
256 '<dt class="list-header">Toolbar group</dt>',
257 '<dd class="list-header">Unused items</dd>'
258 ].join( '' );
259
260 var header = '<h3>Unused toolbar items</h3>';
261
262 if ( !unused.length ) {
263 listHeader = '<p>All items are in use.</p>';
264 }
265
266 this.codeContainer.refresh();
267
268 this.hintContainer.setHtml( header + '<dl>' + listHeader + unusedElements + '</dl>' );
269 };
270
271 /**
272 * @param {String} buttonName
273 * @returns {String}
274 */
275 ToolbarTextModifier.prototype.getToolbarGroupByButtonName = function( buttonName ) {
276 var buttonNames = this.fullToolbarEditor.buttonNamesByGroup;
277
278 for ( var groupName in buttonNames ) {
279 var buttons = buttonNames[ groupName ];
280
281 var i = buttons.length;
282 while ( i-- ) {
283 if ( buttonName === buttons[ i ] ) {
284 return groupName;
285 }
286 }
287
288 }
289
290 return null;
291 };
292
293 /**
294 * Filter all available toolbar elements by array of elements provided in first argument.
295 * Returns elements which are not used.
296 *
297 * @param {Object} toolbar
298 * @param {Boolean} [sorted=false]
299 * @param {String} prefix
300 * @returns {Array}
301 */
302 ToolbarTextModifier.prototype.getUnusedButtonsArray = function( toolbar, sorted, prefix ) {
303 sorted = ( sorted === true ? true : false );
304 var providedElements = ToolbarTextModifier.mapToolbarCfgToElementsList( toolbar ),
305 allElements = Object.keys( this.fullToolbarEditor.editorInstance.ui.items );
306
307 // get rid of "-" elements
308 allElements = FullToolbarEditor.filter( allElements, function( elem ) {
309 var isSeparator = ( elem === '-' ),
310 matchPrefix = ( prefix === undefined || elem.toLowerCase().indexOf( prefix.toLowerCase() ) === 0 );
311
312 return !isSeparator && matchPrefix;
313 } );
314
315 var elementsNotUsed = FullToolbarEditor.filter( allElements, function( elem ) {
316 return CKEDITOR.tools.indexOf( providedElements, elem ) == -1;
317 } );
318
319 if ( sorted )
320 elementsNotUsed.sort();
321
322 return elementsNotUsed;
323 };
324
325 /**
326 *
327 * @param {Array} buttons
328 * @returns {Array}
329 */
330 ToolbarTextModifier.prototype.groupButtonNamesByGroup = function( buttons ) {
331 var result = [],
332 groupedBtns = JSON.parse( JSON.stringify( this.fullToolbarEditor.buttonNamesByGroup ) );
333
334 for ( var groupName in groupedBtns ) {
335 var currGroup = groupedBtns[ groupName ];
336 currGroup = FullToolbarEditor.filter( currGroup, function( btnName ) {
337 return CKEDITOR.tools.indexOf( buttons, btnName ) !== -1;
338 } );
339
340 if ( currGroup.length ) {
341 result.push( {
342 name: groupName,
343 buttons: currGroup
344 } );
345 }
346
347 }
348
349 return result;
350 };
351
352 /**
353 * Map toolbar config value to flat items list.
354 *
355 * input:
356 * [
357 * { name: "basicstyles", items: ["Bold", "Italic"] },
358 * { name: "advancedstyles", items: ["Bold", "Outdent", "Indent"] }
359 * ]
360 *
361 * output:
362 * ["Bold", "Italic", "Outdent", "Indent"]
363 *
364 * @param {Object} toolbar
365 * @returns {Array}
366 */
367 ToolbarTextModifier.mapToolbarCfgToElementsList = function( toolbar ) {
368 var elements = [];
369
370 var max = toolbar.length;
371 for ( var i = 0; i < max; i += 1 ) {
372 if ( !toolbar[ i ] || typeof toolbar[ i ] === 'string' )
373 continue;
374
375 elements = elements.concat( FullToolbarEditor.filter( toolbar[ i ].items, checker ) );
376 }
377
378 function checker( elem ) {
379 return elem !== '-';
380 }
381
382 return elements;
383 };
384
385 /**
386 * @param {String} cfg
387 * @private
388 */
389 ToolbarTextModifier.prototype._setupActualConfig = function( cfg ) {
390 cfg = cfg || this.editorInstance.config;
391
392 // if toolbar already exists in config, there is nothing to do
393 if ( CKEDITOR.tools.isArray( cfg.toolbar ) )
394 return;
395
396 // if toolbar group not present, we need to pick them from full toolbar instance
397 if ( !cfg.toolbarGroups )
398 cfg.toolbarGroups = this.fullToolbarEditor.getFullToolbarGroupsConfig( true );
399
400 this._fixGroups( cfg );
401
402 cfg.toolbar = this._mapToolbarGroupsToToolbar( cfg.toolbarGroups, this.actualConfig.removeButtons );
403
404 this.actualConfig.toolbar = cfg.toolbar;
405 this.actualConfig.removeButtons = '';
406 };
407
408 /**
409 * **Please note:** This method modify element provided in first argument.
410 *
411 * @param {Array} toolbarGroups
412 * @returns {Array}
413 * @private
414 */
415 ToolbarTextModifier.prototype._mapToolbarGroupsToToolbar = function( toolbarGroups, removedBtns ) {
416 removedBtns = removedBtns || this.editorInstance.config.removedBtns;
417 removedBtns = typeof removedBtns == 'string' ? removedBtns.split( ',' ) : [];
418
419 // from the end, because array indexes may change
420 var i = toolbarGroups.length;
421 while ( i-- ) {
422 var mappedSubgroup = this._mapToolbarSubgroup( toolbarGroups[ i ], removedBtns );
423
424 if ( toolbarGroups[ i ].type === 'separator' ) {
425 toolbarGroups[ i ] = '/';
426 continue;
427 }
428
429 // don't want empty groups
430 if ( CKEDITOR.tools.isArray( mappedSubgroup ) && mappedSubgroup.length === 0 ) {
431 toolbarGroups.splice( i, 1 );
432 continue;
433 }
434
435 if ( typeof mappedSubgroup == 'string' )
436 toolbarGroups[ i ] = mappedSubgroup;
437 else {
438 toolbarGroups[ i ] = {
439 name: toolbarGroups[ i ].name,
440 items: mappedSubgroup
441 };
442 }
443 }
444
445 return toolbarGroups;
446 };
447
448 /**
449 *
450 * @param {String|Object} group
451 * @param {Array} removedBtns
452 * @returns {Array}
453 * @private
454 */
455 ToolbarTextModifier.prototype._mapToolbarSubgroup = function( group, removedBtns ) {
456 var totalBtns = 0;
457 if ( typeof group == 'string' )
458 return group;
459
460 var max = group.groups ? group.groups.length : 0,
461 result = [];
462 for ( var i = 0; i < max; i += 1 ) {
463 var currSubgroup = group.groups[ i ];
464
465 var buttons = this.fullToolbarEditor.buttonsByGroup[ typeof currSubgroup === 'string' ? currSubgroup : currSubgroup.name ] || [];
466 buttons = this._mapButtonsToButtonsNames( buttons, removedBtns );
467 var currTotalBtns = buttons.length;
468 totalBtns += currTotalBtns;
469 result = result.concat( buttons );
470
471 if ( currTotalBtns )
472 result.push( '-' );
473 }
474
475 if ( result[ result.length - 1 ] == '-' )
476 result.pop();
477
478 return result;
479 };
480
481 /**
482 *
483 * @param {Array} buttons
484 * @param {Array} removedBtns
485 * @returns {Array}
486 * @private
487 */
488 ToolbarTextModifier.prototype._mapButtonsToButtonsNames = function( buttons, removedBtns ) {
489 var i = buttons.length;
490 while ( i-- ) {
491 var currBtn = buttons[ i ],
492 camelCasedName;
493
494 if ( typeof currBtn === 'string' ) {
495 camelCasedName = currBtn;
496 } else {
497 camelCasedName = this.fullToolbarEditor.getCamelCasedButtonName( currBtn.name );
498 }
499
500 if ( CKEDITOR.tools.indexOf( removedBtns, camelCasedName ) !== -1 ) {
501 buttons.splice( i, 1 );
502 continue;
503 }
504
505 buttons[ i ] = camelCasedName;
506 }
507
508 return buttons;
509 };
510
511 /**
512 * @param {String} val
513 * @returns {Object}
514 * @private
515 */
516 ToolbarTextModifier.prototype._evaluateValue = function( val ) {
517 var parsed;
518
519 try {
520 var config = {};
521 ( function() {
522 var CKEDITOR = Function( 'var CKEDITOR = {}; ' + val + '; return CKEDITOR;' )();
523
524 CKEDITOR.editorConfig( config );
525 parsed = config;
526 } )();
527
528 // CKEditor does not handle empty arrays in configuration files
529 // on IE8
530 var i = parsed.toolbar.length;
531 while ( i-- )
532 if ( !parsed.toolbar[ i ] ) parsed.toolbar.splice( i, 1 );
533
534 } catch ( e ) {
535 parsed = null;
536 }
537
538 return parsed;
539 };
540
541 /**
542 * @param {Array} toolbar
543 * @returns {{toolbarGroups: Array, removeButtons: string}}
544 */
545 ToolbarTextModifier.prototype.mapToolbarToToolbarGroups = function( toolbar ) {
546 var usedGroups = {},
547 removeButtons = [],
548 toolbarGroups = [];
549
550 var max = toolbar.length;
551 for ( var i = 0; i < max; i++ ) {
552 if ( toolbar[ i ] === '/' ) {
553 toolbarGroups.push( '/' );
554 continue;
555 }
556
557 var items = toolbar[ i ].items;
558
559 var toolbarGroup = {};
560 toolbarGroup.name = toolbar[ i ].name;
561 toolbarGroup.groups = [];
562
563 var max2 = items.length;
564 for ( var j = 0; j < max2; j++ ) {
565 var item = items[ j ];
566
567 if ( item === '-' ) {
568 continue;
569 }
570
571 var groupName = this.getToolbarGroupByButtonName( item );
572
573 var groupIndex = toolbarGroup.groups.indexOf( groupName );
574 if ( groupIndex === -1 ) {
575 toolbarGroup.groups.push( groupName );
576 }
577
578 usedGroups[ groupName ] = usedGroups[ groupName ] || {};
579
580 var buttons = ( usedGroups[ groupName ].buttons = usedGroups[ groupName ].buttons || {} );
581
582 buttons[ item ] = buttons[ item ] || { used: 0, origin: toolbarGroup.name };
583 buttons[ item ].used++;
584 }
585
586 toolbarGroups.push( toolbarGroup );
587 }
588
589 // Handling removed buttons
590 removeButtons = prepareRemovedButtons( usedGroups, this.fullToolbarEditor.buttonNamesByGroup );
591
592 function prepareRemovedButtons( usedGroups, buttonNames ) {
593 var removed = [];
594
595 for ( var groupName in usedGroups ) {
596 var group = usedGroups[ groupName ];
597 var allButtonsInGroup = buttonNames[ groupName ].slice();
598
599 removed = removed.concat( removeStuffFromArray( allButtonsInGroup, Object.keys( group.buttons ) ) );
600 }
601
602 return removed;
603 }
604
605 function removeStuffFromArray( array, stuff ) {
606 array = array.slice();
607 var i = stuff.length;
608
609 while ( i-- ) {
610 var atIndex = array.indexOf( stuff[ i ] );
611 if ( atIndex !== -1 ) {
612 array.splice( atIndex, 1 );
613 }
614 }
615
616 return array;
617 }
618
619 return { toolbarGroups: toolbarGroups, removeButtons: removeButtons.join( ',' ) };
620 };
621
622 return ToolbarTextModifier;
623} )();
diff --git a/sources/samples/toolbarconfigurator/less/base.less b/sources/samples/toolbarconfigurator/less/base.less
new file mode 100644
index 00000000..be59d207
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/less/base.less
@@ -0,0 +1,38 @@
1// Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
2// For licensing, see LICENSE.html or http://cksource.com/ckeditor/license
3
4@base-font-size: 16px;
5@base-line-height: 24px;
6@base-line-ratio: 1.8;
7
8@sample-font-stack: Arial, Helvetica, sans-serif;
9@sample-font-stack-monospace: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
10@sample-font-maven: "Maven Pro";
11@sample-font-indie: "Indie Flower";
12@sample-text-color: #575757;
13
14@sample-link-color: #cf5d4e;
15@sample-link-color-hover: lighten( @sample-link-color, -10% );
16
17@sample-box-background-color: #f5f5f5;
18@sample-box-border-color: #ddd;
19
20@sample-top-navigation-background: #454545;
21
22// Standard gaps
23@sample-standard-vgap: 1.2em;
24@sample-standard-hgap: 1.5em;
25
26// Generic font-size/line-height mixin.
27.font-size( @remSize ) {
28 @pxSize: round( @remSize * @base-font-size, 2 );
29
30 @remHeight: round( @remSize * @base-line-ratio, 2 );
31 @pxHeight: round( @pxSize * @base-line-ratio, 2 );
32
33 font-size: ~"@{pxSize}";
34 font-size: ~"@{remSize}rem";
35
36 line-height: ~"@{pxHeight}";
37 line-height: ~"@{remHeight}rem";
38}
diff --git a/sources/samples/toolbarconfigurator/less/toolbarmodifier.less b/sources/samples/toolbarconfigurator/less/toolbarmodifier.less
new file mode 100644
index 00000000..ea6ed15a
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/less/toolbarmodifier.less
@@ -0,0 +1,508 @@
1// Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
2// For licensing, see LICENSE.html or http://cksource.com/ckeditor/license
3
4@import "base.less";
5
6@modifier-group-hover-color: #fffbe3;
7@modifier-group-active-color: #f0fafb;
8
9@modifier-active-toolbar-color: darken( @modifier-group-hover-color, 10% );
10
11@modifier-toolbar-border-color: #ccc;
12@modifier-toolbar-group-border-color: #ddd;
13@modifier-toolbar-group-vpadding: 2px;
14@modifier-toolbar-hgap: 5px;
15
16@modifier-toolbar-button-color: #e7e7e7;
17
18#toolbar .cke_toolbar {
19 pointer-events: none;
20 .user-select( none );
21 cursor: default;
22}
23
24// Dim all but active toolbars if some is active.
25.some-toolbar-active .cke_toolbar {
26 .opacity( .5 );
27}
28
29.cke_toolbar.active {
30 position: relative;
31
32 // Active toolbar is always highlighted.
33 .opacity( 1 );
34
35 &:after {
36 content: '';
37 display: block;
38 position: absolute;
39 top: 0;
40 right: 6px;
41 bottom: 5px;
42 left: 0;
43 .border-radius( 5px );
44 .box-shadow( 0px 0px 15px 3px @modifier-active-toolbar-color );
45 }
46
47 .cke_toolgroup {
48 .box-shadow( none );
49 border-color: darken( @modifier-active-toolbar-color, 40% );
50 }
51
52 .cke_combo,
53 .cke_toolgroup {
54 position: relative;
55 z-index: 2;
56 }
57
58 .cke_combo_button {
59 .box-shadow( none );
60 }
61}
62
63.unselectable {
64 .user-select( none );
65}
66.toolbar {
67 padding: 5px 0;
68 margin-bottom: 2 * @sample-standard-vgap;
69 overflow: hidden;
70 background: #fff;
71
72 button.button-a {
73 &.cke_button {
74 cursor: pointer;
75
76 display: inline-block;
77 padding: 4px 6px;
78 outline: 0;
79 border: 1px solid #a6a6a6;
80 }
81
82 &.hidden {
83 display: none;
84 }
85
86 &.left {
87 float: left;
88 margin-right: 8px;
89 }
90
91 &.right {
92 float: right;
93 margin-left: 8px;
94 }
95
96 .highlight {
97 color: #ffefc1;
98 }
99 }
100}
101
102// Styles applied when configurator is hidden and code is being displayed (and vice-versa).
103.configContainer.hidden,
104.toolbarModifier.hidden,
105.toolbarModifier-hints.hidden {
106 display: none;
107}
108
109.toolbarModifier :focus,
110.toolbar button:focus,
111.configContainer textarea.configCode:focus {
112 outline: none;
113}
114
115div.toolbarModifier {
116 padding: 0;
117 overflow: hidden;
118 width: 100%;
119 position: relative;
120 display: table;
121 border-collapse: collapse;
122
123 ::-moz-focus-inner {
124 border: 0;
125 }
126
127 .empty {
128 display: none;
129 }
130
131 &.empty-visible .empty {
132 display: table-row;
133 .opacity( 0.6 );
134 }
135
136 // Give empty toolbar groups height similar to height of non empty groups.
137 // Non empty groups are stretched by contained toolbar buttons.
138 .empty > p {
139 line-height: 31px;
140 }
141
142 // List of toolbars.
143 & > ul {
144 padding: 0;
145 margin: 0;
146 border-top: 1px solid @modifier-toolbar-border-color;
147 width: 100%;
148
149 &[data-type="table-header"] {
150 display: table-header-group;
151 }
152
153 &[data-type="table-body"] {
154 display: table-row-group;
155 }
156
157 // Override global margins and paddings.
158 p {
159 padding: 0;
160 margin: 0;
161 }
162
163 // A single toolbar.
164 & > li {
165 display: table-row;
166
167 &[data-type="header"] {
168 font-weight: bold;
169 user-select: none;
170 cursor: default;
171 }
172
173 &[data-type="group"],
174 &[data-type="separator"] {
175 border-bottom: 1px solid @modifier-toolbar-border-color;
176 }
177
178 &[data-type="subgroup"] {
179 border-top: 1px solid #eee;
180
181 &:first-child {
182 border-top: none;
183 }
184 }
185
186 &[data-type="group"].active,
187 &[data-type="group"]:hover,
188 &[data-type="separator"].active,
189 &[data-type="separator"]:hover {
190 overflow: hidden;
191 z-index: 2;
192 }
193
194 &[data-type="group"].active,
195 &[data-type="separator"].active,
196 &[data-type="group"].active:hover,
197 &[data-type="separator"].active:hover {
198 background: @modifier-group-active-color;
199 }
200
201 &[data-type="group"]:hover,
202 &[data-type="separator"]:hover {
203 background: @modifier-group-hover-color;
204 }
205
206 &[data-type="separator"] {
207 &:after {
208 content: '';
209 width: 100%;
210 }
211
212 background: #f5f5f5;
213
214 & > p {
215 padding: @modifier-toolbar-group-vpadding @modifier-toolbar-hgap;
216 }
217 }
218
219 & > p, & > ul {
220 display: table-cell;
221 vertical-align: middle;
222 }
223
224 // Note: this also controls the list of toolbar groups.
225 p {
226 padding-left: @modifier-toolbar-hgap;
227 min-width: 200px;
228
229 span {
230 white-space: nowrap;
231 cursor: default;
232
233 button {
234 font-size: 12.666px;
235 margin-right: 5px;
236 cursor: pointer;
237 background: #fff;
238 .border-radius( 5px );
239 border: 1px solid #bbb;
240 padding: 0 7px;
241 line-height: 12px;
242 height: 20px;
243
244 &:not(.disabled) {
245 &:hover,
246 &:focus {
247 color: #fff;
248 background-color: @sample-top-navigation-background;
249 border-color: transparent;
250 }
251 }
252
253 &.move.disabled {
254 cursor: default;
255 .opacity( 0.2 );
256 }
257 }
258 }
259 }
260
261 // List of toolbar groups.
262 ul {
263 border-collapse: collapse;
264 padding: 0;
265 width: 100%;
266
267 // A single toolbar group.
268 li {
269 display: table-row;
270 list-style-type: none;
271 // Resets slightly increased lists' lh which is bigger than button's height
272 // so it stretches columns.
273 line-height: 1;
274
275 &[data-type="subgroup"] {
276 border-top: 1px solid @modifier-toolbar-group-border-color;
277
278 &:first-child {
279 border-top: 0;
280 }
281
282 [data-type="button"] {
283 .border-radius( 3px );
284 padding: 0 2px;
285
286 &:focus {
287 background: rgba(0, 0, 0, 0.04);
288 }
289
290 input {
291 vertical-align: middle;
292 }
293 }
294 }
295
296 & > p, & > ul {
297 display: table-cell;
298 vertical-align: middle;
299 }
300
301 // List of buttons in a group.
302 ul {
303 padding: 0;
304
305 // A single button in a group.
306 li {
307 padding: 0;
308 display: inline-block;
309 cursor: pointer;
310 margin: @modifier-toolbar-group-vpadding 5px @modifier-toolbar-group-vpadding 0;
311
312 // Enforce styles to save space.
313 .cke_combo_text {
314 cursor: pointer;
315 white-space: nowrap;
316 }
317
318 .cke_toolgroup,
319 .cke_combo_button {
320 cursor: pointer;
321 margin: 0;
322 vertical-align: middle;
323 border: 1px solid #ddd;
324 .font-size( .713 );
325 }
326 }
327 }
328 }
329 }
330 }
331 }
332
333 & > .codemirror-wrapper {
334 overflow-y: auto;
335 }
336
337 // Advanced configurator: list of unused elements.
338 &-hints {
339 float: right;
340 width: 350px;
341 min-width: 150px;
342 overflow-y: auto;
343 margin-left: @sample-standard-hgap;
344
345 h3 {
346 .font-size( 1.13 );
347 padding: .3*@sample-standard-vgap @sample-standard-hgap;
348 background: @sample-box-background-color;
349 border-bottom: 1px solid @sample-box-border-color;
350 margin-top: 0;
351 margin-bottom: @sample-standard-vgap;
352 }
353
354 dl {
355 //margin-top: 0;
356 margin-bottom: @sample-standard-vgap;
357 overflow: hidden;
358
359 .list-header {
360 font-weight: bold;
361 border: 0;
362 padding-bottom: .5*@sample-standard-vgap;
363 }
364
365 & > p {
366 text-align: center;
367 }
368
369 dt {
370 float: left;
371 width: 9em;
372 clear: both;
373 text-align: right;
374 border-top: 1px solid @sample-box-border-color;
375 padding-left: @sample-standard-hgap;
376 padding-right: .1em;
377 .box-sizing( border-box );
378
379 code {
380 background: none;
381 border: none;
382 vertical-align: middle;
383 }
384 }
385
386 dd {
387 margin-left: 10em;
388 clear: right;
389 padding-right: @sample-standard-hgap;
390
391 code {
392 line-height: 2.2em;
393 }
394
395 &:after {
396 content: '\00a0';
397 display: block;
398 clear: left;
399 float: right;
400 height: 0;
401 width: 0;
402 }
403 }
404 }
405 }
406}
407
408.toolbarModifier-hints,
409.configContainer textarea.configCode,
410.CodeMirror {
411 .border-radius( 3px );
412 border: 1px solid #ccc;
413 .font-size( .813 );
414}
415
416.configContainer textarea.configCode,
417.CodeMirror pre,
418.CodeMirror-linenumber {
419 .font-size( .813 );
420 font-family: @sample-font-stack-monospace;
421}
422
423.CodeMirror pre {
424 border: none;
425 padding: 0;
426 margin: 0;
427}
428
429.configContainer textarea.configCode {
430 .box-sizing( border-box );
431 color: @sample-text-color;
432 padding: 10px;
433 width: 100%;
434 min-height: 500px;
435 margin: 0;
436 resize: none;
437 outline: none;
438 -moz-tab-size: 4;
439 tab-size: 4;
440 white-space: pre;
441 word-wrap: normal;
442 overflow: auto;
443}
444
445.CodeMirror-hints.toolbar-modifier {
446 padding: 0;
447 color: @sample-text-color;
448
449 .CodeMirror-hint-active {
450 color: @sample-text-color;
451 background: @modifier-group-active-color;
452 }
453
454 .font-size( .875 );
455 font-family: @sample-font-stack-monospace;
456
457 & > li:hover {
458 background: @modifier-group-hover-color;
459 }
460}
461
462/* Text modifier */
463#toolbarModifierWrapper {
464 margin-bottom: @sample-standard-vgap;
465
466 .invalid .CodeMirror {
467 background: #fff8f8;
468 border-color: red;
469 }
470
471 .CodeMirror {
472 // Autogrow. http://codemirror.net/demo/resize.html
473 height: auto;
474 // Complementory with std's CodeMirror-lines vertical padding.
475 // Not needed when we use lines number, but we can't due to a bug in CM.
476 padding: 0 @sample-standard-vgap/2;
477 }
478}
479
480.staticContainer {
481 position: fixed;
482 top: 0;
483 width: 100%;
484 z-index: 10;
485
486 > .grid-container {
487 max-width: 1044px + 2 * @grid-gutter-width;
488
489 .inner {
490 background: #fff;
491
492 .toolbar {
493 margin-bottom: 0;
494 }
495 }
496 }
497}
498
499// Help button to display information about configurator.
500#help {
501 position: relative;
502 top: -15px;
503 left: -5px;
504
505 &-content {
506 display: none;
507 }
508}
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/LICENSE b/sources/samples/toolbarconfigurator/lib/codemirror/LICENSE
new file mode 100644
index 00000000..d21bbea5
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/LICENSE
@@ -0,0 +1,19 @@
1Copyright (C) 2014 by Marijn Haverbeke <marijnh@gmail.com> and others
2
3Permission is hereby granted, free of charge, to any person obtaining a copy
4of this software and associated documentation files (the "Software"), to deal
5in the Software without restriction, including without limitation the rights
6to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7copies of the Software, and to permit persons to whom the Software is
8furnished to do so, subject to the following conditions:
9
10The above copyright notice and this permission notice shall be included in
11all copies or substantial portions of the Software.
12
13THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19THE SOFTWARE.
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/README.md b/sources/samples/toolbarconfigurator/lib/codemirror/README.md
new file mode 100644
index 00000000..38156a74
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/README.md
@@ -0,0 +1,12 @@
1# CodeMirror
2[![Build Status](https://travis-ci.org/codemirror/CodeMirror.svg)](https://travis-ci.org/codemirror/CodeMirror)
3[![NPM version](https://img.shields.io/npm/v/codemirror.svg)](https://www.npmjs.org/package/codemirror)
4[Funding status: ![maintainer happiness](https://marijnhaverbeke.nl/fund/status_s.png?again)](https://marijnhaverbeke.nl/fund/)
5
6CodeMirror is a JavaScript component that provides a code editor in
7the browser. When a mode is available for the language you are coding
8in, it will color your code, and optionally help with indentation.
9
10The project page is http://codemirror.net
11The manual is at http://codemirror.net/doc/manual.html
12The contributing guidelines are in [CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.css b/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.css
new file mode 100644
index 00000000..ceacd130
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.css
@@ -0,0 +1,325 @@
1/* BASICS */
2
3.CodeMirror {
4 /* Set height, width, borders, and global font properties here */
5 font-family: monospace;
6 height: 300px;
7 color: black;
8}
9
10/* PADDING */
11
12.CodeMirror-lines {
13 padding: 4px 0; /* Vertical padding around content */
14}
15.CodeMirror pre {
16 padding: 0 4px; /* Horizontal padding of content */
17}
18
19.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
20 background-color: white; /* The little square between H and V scrollbars */
21}
22
23/* GUTTER */
24
25.CodeMirror-gutters {
26 border-right: 1px solid #ddd;
27 background-color: #f7f7f7;
28 white-space: nowrap;
29}
30.CodeMirror-linenumbers {}
31.CodeMirror-linenumber {
32 padding: 0 3px 0 5px;
33 min-width: 20px;
34 text-align: right;
35 color: #999;
36 white-space: nowrap;
37}
38
39.CodeMirror-guttermarker { color: black; }
40.CodeMirror-guttermarker-subtle { color: #999; }
41
42/* CURSOR */
43
44.CodeMirror div.CodeMirror-cursor {
45 border-left: 1px solid black;
46}
47/* Shown when moving in bi-directional text */
48.CodeMirror div.CodeMirror-secondarycursor {
49 border-left: 1px solid silver;
50}
51.CodeMirror.cm-fat-cursor div.CodeMirror-cursor {
52 width: auto;
53 border: 0;
54 background: #7e7;
55}
56.CodeMirror.cm-fat-cursor div.CodeMirror-cursors {
57 z-index: 1;
58}
59
60.cm-animate-fat-cursor {
61 width: auto;
62 border: 0;
63 -webkit-animation: blink 1.06s steps(1) infinite;
64 -moz-animation: blink 1.06s steps(1) infinite;
65 animation: blink 1.06s steps(1) infinite;
66}
67@-moz-keyframes blink {
68 0% { background: #7e7; }
69 50% { background: none; }
70 100% { background: #7e7; }
71}
72@-webkit-keyframes blink {
73 0% { background: #7e7; }
74 50% { background: none; }
75 100% { background: #7e7; }
76}
77@keyframes blink {
78 0% { background: #7e7; }
79 50% { background: none; }
80 100% { background: #7e7; }
81}
82
83/* Can style cursor different in overwrite (non-insert) mode */
84div.CodeMirror-overwrite div.CodeMirror-cursor {}
85
86.cm-tab { display: inline-block; text-decoration: inherit; }
87
88.CodeMirror-ruler {
89 border-left: 1px solid #ccc;
90 position: absolute;
91}
92
93/* DEFAULT THEME */
94
95.cm-s-default .cm-keyword {color: #708;}
96.cm-s-default .cm-atom {color: #219;}
97.cm-s-default .cm-number {color: #164;}
98.cm-s-default .cm-def {color: #00f;}
99.cm-s-default .cm-variable,
100.cm-s-default .cm-punctuation,
101.cm-s-default .cm-property,
102.cm-s-default .cm-operator {}
103.cm-s-default .cm-variable-2 {color: #05a;}
104.cm-s-default .cm-variable-3 {color: #085;}
105.cm-s-default .cm-comment {color: #a50;}
106.cm-s-default .cm-string {color: #a11;}
107.cm-s-default .cm-string-2 {color: #f50;}
108.cm-s-default .cm-meta {color: #555;}
109.cm-s-default .cm-qualifier {color: #555;}
110.cm-s-default .cm-builtin {color: #30a;}
111.cm-s-default .cm-bracket {color: #997;}
112.cm-s-default .cm-tag {color: #170;}
113.cm-s-default .cm-attribute {color: #00c;}
114.cm-s-default .cm-header {color: blue;}
115.cm-s-default .cm-quote {color: #090;}
116.cm-s-default .cm-hr {color: #999;}
117.cm-s-default .cm-link {color: #00c;}
118
119.cm-negative {color: #d44;}
120.cm-positive {color: #292;}
121.cm-header, .cm-strong {font-weight: bold;}
122.cm-em {font-style: italic;}
123.cm-link {text-decoration: underline;}
124.cm-strikethrough {text-decoration: line-through;}
125
126.cm-s-default .cm-error {color: #f00;}
127.cm-invalidchar {color: #f00;}
128
129.CodeMirror-composing { border-bottom: 2px solid; }
130
131/* Default styles for common addons */
132
133div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
134div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
135.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
136.CodeMirror-activeline-background {background: #e8f2ff;}
137
138/* STOP */
139
140/* The rest of this file contains styles related to the mechanics of
141 the editor. You probably shouldn't touch them. */
142
143.CodeMirror {
144 position: relative;
145 overflow: hidden;
146 background: white;
147}
148
149.CodeMirror-scroll {
150 overflow: scroll !important; /* Things will break if this is overridden */
151 /* 30px is the magic margin used to hide the element's real scrollbars */
152 /* See overflow: hidden in .CodeMirror */
153 margin-bottom: -30px; margin-right: -30px;
154 padding-bottom: 30px;
155 height: 100%;
156 outline: none; /* Prevent dragging from highlighting the element */
157 position: relative;
158}
159.CodeMirror-sizer {
160 position: relative;
161 border-right: 30px solid transparent;
162}
163
164/* The fake, visible scrollbars. Used to force redraw during scrolling
165 before actuall scrolling happens, thus preventing shaking and
166 flickering artifacts. */
167.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
168 position: absolute;
169 z-index: 6;
170 display: none;
171}
172.CodeMirror-vscrollbar {
173 right: 0; top: 0;
174 overflow-x: hidden;
175 overflow-y: scroll;
176}
177.CodeMirror-hscrollbar {
178 bottom: 0; left: 0;
179 overflow-y: hidden;
180 overflow-x: scroll;
181}
182.CodeMirror-scrollbar-filler {
183 right: 0; bottom: 0;
184}
185.CodeMirror-gutter-filler {
186 left: 0; bottom: 0;
187}
188
189.CodeMirror-gutters {
190 position: absolute; left: 0; top: 0;
191 z-index: 3;
192}
193.CodeMirror-gutter {
194 white-space: normal;
195 height: 100%;
196 display: inline-block;
197 margin-bottom: -30px;
198 /* Hack to make IE7 behave */
199 *zoom:1;
200 *display:inline;
201}
202.CodeMirror-gutter-wrapper {
203 position: absolute;
204 z-index: 4;
205 height: 100%;
206}
207.CodeMirror-gutter-elt {
208 position: absolute;
209 cursor: default;
210 z-index: 4;
211}
212.CodeMirror-gutter-wrapper {
213 -webkit-user-select: none;
214 -moz-user-select: none;
215 user-select: none;
216}
217
218.CodeMirror-lines {
219 cursor: text;
220 min-height: 1px; /* prevents collapsing before first draw */
221}
222.CodeMirror pre {
223 /* Reset some styles that the rest of the page might have set */
224 -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
225 border-width: 0;
226 background: transparent;
227 font-family: inherit;
228 font-size: inherit;
229 margin: 0;
230 white-space: pre;
231 word-wrap: normal;
232 line-height: inherit;
233 color: inherit;
234 z-index: 2;
235 position: relative;
236 overflow: visible;
237 -webkit-tap-highlight-color: transparent;
238}
239.CodeMirror-wrap pre {
240 word-wrap: break-word;
241 white-space: pre-wrap;
242 word-break: normal;
243}
244
245.CodeMirror-linebackground {
246 position: absolute;
247 left: 0; right: 0; top: 0; bottom: 0;
248 z-index: 0;
249}
250
251.CodeMirror-linewidget {
252 position: relative;
253 z-index: 2;
254 overflow: auto;
255}
256
257.CodeMirror-widget {}
258
259.CodeMirror-code {
260 outline: none;
261}
262
263/* Force content-box sizing for the elements where we expect it */
264.CodeMirror-scroll,
265.CodeMirror-sizer,
266.CodeMirror-gutter,
267.CodeMirror-gutters,
268.CodeMirror-linenumber {
269 -moz-box-sizing: content-box;
270 box-sizing: content-box;
271}
272
273.CodeMirror-measure {
274 position: absolute;
275 width: 100%;
276 height: 0;
277 overflow: hidden;
278 visibility: hidden;
279}
280.CodeMirror-measure pre { position: static; }
281
282.CodeMirror div.CodeMirror-cursor {
283 position: absolute;
284 border-right: none;
285 width: 0;
286}
287
288div.CodeMirror-cursors {
289 visibility: hidden;
290 position: relative;
291 z-index: 3;
292}
293.CodeMirror-focused div.CodeMirror-cursors {
294 visibility: visible;
295}
296
297.CodeMirror-selected { background: #d9d9d9; }
298.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
299.CodeMirror-crosshair { cursor: crosshair; }
300.CodeMirror ::selection { background: #d7d4f0; }
301.CodeMirror ::-moz-selection { background: #d7d4f0; }
302
303.cm-searching {
304 background: #ffa;
305 background: rgba(255, 255, 0, .4);
306}
307
308/* IE7 hack to prevent it from returning funny offsetTops on the spans */
309.CodeMirror span { *vertical-align: text-bottom; }
310
311/* Used to force a border model for a node */
312.cm-force-border { padding-right: .1px; }
313
314@media print {
315 /* Hide the cursor when printing */
316 .CodeMirror div.CodeMirror-cursors {
317 visibility: hidden;
318 }
319}
320
321/* See issue #2901 */
322.cm-tab-wrap-hack:after { content: ''; }
323
324/* Help users use markselection to safely style text background */
325span.CodeMirror-selectedtext { background: none; }
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.js b/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.js
new file mode 100644
index 00000000..37e26855
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/codemirror.js
@@ -0,0 +1,8738 @@
1// CodeMirror, copyright (c) by Marijn Haverbeke and others
2// Distributed under an MIT license: http://codemirror.net/LICENSE
3
4// This is CodeMirror (http://codemirror.net), a code editor
5// implemented in JavaScript on top of the browser's DOM.
6//
7// You can find some technical background for some of the code below
8// at http://marijnhaverbeke.nl/blog/#cm-internals .
9
10(function(mod) {
11 if (typeof exports == "object" && typeof module == "object") // CommonJS
12 module.exports = mod();
13 else if (typeof define == "function" && define.amd) // AMD
14 return define([], mod);
15 else // Plain browser env
16 this.CodeMirror = mod();
17})(function() {
18 "use strict";
19
20 // BROWSER SNIFFING
21
22 // Kludges for bugs and behavior differences that can't be feature
23 // detected are enabled based on userAgent etc sniffing.
24
25 var gecko = /gecko\/\d/i.test(navigator.userAgent);
26 var ie_upto10 = /MSIE \d/.test(navigator.userAgent);
27 var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
28 var ie = ie_upto10 || ie_11up;
29 var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
30 var webkit = /WebKit\//.test(navigator.userAgent);
31 var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
32 var chrome = /Chrome\//.test(navigator.userAgent);
33 var presto = /Opera\//.test(navigator.userAgent);
34 var safari = /Apple Computer/.test(navigator.vendor);
35 var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent);
36 var phantom = /PhantomJS/.test(navigator.userAgent);
37
38 var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
39 // This is woefully incomplete. Suggestions for alternative methods welcome.
40 var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
41 var mac = ios || /Mac/.test(navigator.platform);
42 var windows = /win/i.test(navigator.platform);
43
44 var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
45 if (presto_version) presto_version = Number(presto_version[1]);
46 if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
47 // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
48 var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
49 var captureRightClick = gecko || (ie && ie_version >= 9);
50
51 // Optimize some code when these features are not used.
52 var sawReadOnlySpans = false, sawCollapsedSpans = false;
53
54 // EDITOR CONSTRUCTOR
55
56 // A CodeMirror instance represents an editor. This is the object
57 // that user code is usually dealing with.
58
59 function CodeMirror(place, options) {
60 if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
61
62 this.options = options = options ? copyObj(options) : {};
63 // Determine effective options based on given values and defaults.
64 copyObj(defaults, options, false);
65 setGuttersForLineNumbers(options);
66
67 var doc = options.value;
68 if (typeof doc == "string") doc = new Doc(doc, options.mode);
69 this.doc = doc;
70
71 var input = new CodeMirror.inputStyles[options.inputStyle](this);
72 var display = this.display = new Display(place, doc, input);
73 display.wrapper.CodeMirror = this;
74 updateGutters(this);
75 themeChanged(this);
76 if (options.lineWrapping)
77 this.display.wrapper.className += " CodeMirror-wrap";
78 if (options.autofocus && !mobile) display.input.focus();
79 initScrollbars(this);
80
81 this.state = {
82 keyMaps: [], // stores maps added by addKeyMap
83 overlays: [], // highlighting overlays, as added by addOverlay
84 modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info
85 overwrite: false,
86 delayingBlurEvent: false,
87 focused: false,
88 suppressEdits: false, // used to disable editing during key handlers when in readOnly mode
89 pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll
90 draggingText: false,
91 highlight: new Delayed(), // stores highlight worker timeout
92 keySeq: null, // Unfinished key sequence
93 specialChars: null
94 };
95
96 var cm = this;
97
98 // Override magic textarea content restore that IE sometimes does
99 // on our hidden textarea on reload
100 if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
101
102 registerEventHandlers(this);
103 ensureGlobalHandlers();
104
105 startOperation(this);
106 this.curOp.forceUpdate = true;
107 attachDoc(this, doc);
108
109 if ((options.autofocus && !mobile) || cm.hasFocus())
110 setTimeout(bind(onFocus, this), 20);
111 else
112 onBlur(this);
113
114 for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
115 optionHandlers[opt](this, options[opt], Init);
116 maybeUpdateLineNumberWidth(this);
117 if (options.finishInit) options.finishInit(this);
118 for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
119 endOperation(this);
120 // Suppress optimizelegibility in Webkit, since it breaks text
121 // measuring on line wrapping boundaries.
122 if (webkit && options.lineWrapping &&
123 getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
124 display.lineDiv.style.textRendering = "auto";
125 }
126
127 // DISPLAY CONSTRUCTOR
128
129 // The display handles the DOM integration, both for input reading
130 // and content drawing. It holds references to DOM nodes and
131 // display-related state.
132
133 function Display(place, doc, input) {
134 var d = this;
135 this.input = input;
136
137 // Covers bottom-right square when both scrollbars are present.
138 d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
139 d.scrollbarFiller.setAttribute("cm-not-content", "true");
140 // Covers bottom of gutter when coverGutterNextToScrollbar is on
141 // and h scrollbar is present.
142 d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
143 d.gutterFiller.setAttribute("cm-not-content", "true");
144 // Will contain the actual code, positioned to cover the viewport.
145 d.lineDiv = elt("div", null, "CodeMirror-code");
146 // Elements are added to these to represent selection and cursors.
147 d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
148 d.cursorDiv = elt("div", null, "CodeMirror-cursors");
149 // A visibility: hidden element used to find the size of things.
150 d.measure = elt("div", null, "CodeMirror-measure");
151 // When lines outside of the viewport are measured, they are drawn in this.
152 d.lineMeasure = elt("div", null, "CodeMirror-measure");
153 // Wraps everything that needs to exist inside the vertically-padded coordinate system
154 d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
155 null, "position: relative; outline: none");
156 // Moved around its parent to cover visible view.
157 d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
158 // Set to the height of the document, allowing scrolling.
159 d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
160 d.sizerWidth = null;
161 // Behavior of elts with overflow: auto and padding is
162 // inconsistent across browsers. This is used to ensure the
163 // scrollable area is big enough.
164 d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;");
165 // Will contain the gutters, if any.
166 d.gutters = elt("div", null, "CodeMirror-gutters");
167 d.lineGutter = null;
168 // Actual scrollable element.
169 d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
170 d.scroller.setAttribute("tabIndex", "-1");
171 // The element in which the editor lives.
172 d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
173
174 // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
175 if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
176 if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
177
178 if (place) {
179 if (place.appendChild) place.appendChild(d.wrapper);
180 else place(d.wrapper);
181 }
182
183 // Current rendered range (may be bigger than the view window).
184 d.viewFrom = d.viewTo = doc.first;
185 d.reportedViewFrom = d.reportedViewTo = doc.first;
186 // Information about the rendered lines.
187 d.view = [];
188 d.renderedView = null;
189 // Holds info about a single rendered line when it was rendered
190 // for measurement, while not in view.
191 d.externalMeasured = null;
192 // Empty space (in pixels) above the view
193 d.viewOffset = 0;
194 d.lastWrapHeight = d.lastWrapWidth = 0;
195 d.updateLineNumbers = null;
196
197 d.nativeBarWidth = d.barHeight = d.barWidth = 0;
198 d.scrollbarsClipped = false;
199
200 // Used to only resize the line number gutter when necessary (when
201 // the amount of lines crosses a boundary that makes its width change)
202 d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
203 // Set to true when a non-horizontal-scrolling line widget is
204 // added. As an optimization, line widget aligning is skipped when
205 // this is false.
206 d.alignWidgets = false;
207
208 d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
209
210 // Tracks the maximum line length so that the horizontal scrollbar
211 // can be kept static when scrolling.
212 d.maxLine = null;
213 d.maxLineLength = 0;
214 d.maxLineChanged = false;
215
216 // Used for measuring wheel scrolling granularity
217 d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
218
219 // True when shift is held down.
220 d.shift = false;
221
222 // Used to track whether anything happened since the context menu
223 // was opened.
224 d.selForContextMenu = null;
225
226 d.activeTouch = null;
227
228 input.init(d);
229 }
230
231 // STATE UPDATES
232
233 // Used to get the editor into a consistent state again when options change.
234
235 function loadMode(cm) {
236 cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
237 resetModeState(cm);
238 }
239
240 function resetModeState(cm) {
241 cm.doc.iter(function(line) {
242 if (line.stateAfter) line.stateAfter = null;
243 if (line.styles) line.styles = null;
244 });
245 cm.doc.frontier = cm.doc.first;
246 startWorker(cm, 100);
247 cm.state.modeGen++;
248 if (cm.curOp) regChange(cm);
249 }
250
251 function wrappingChanged(cm) {
252 if (cm.options.lineWrapping) {
253 addClass(cm.display.wrapper, "CodeMirror-wrap");
254 cm.display.sizer.style.minWidth = "";
255 cm.display.sizerWidth = null;
256 } else {
257 rmClass(cm.display.wrapper, "CodeMirror-wrap");
258 findMaxLine(cm);
259 }
260 estimateLineHeights(cm);
261 regChange(cm);
262 clearCaches(cm);
263 setTimeout(function(){updateScrollbars(cm);}, 100);
264 }
265
266 // Returns a function that estimates the height of a line, to use as
267 // first approximation until the line becomes visible (and is thus
268 // properly measurable).
269 function estimateHeight(cm) {
270 var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
271 var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
272 return function(line) {
273 if (lineIsHidden(cm.doc, line)) return 0;
274
275 var widgetsHeight = 0;
276 if (line.widgets) for (var i = 0; i < line.widgets.length; i++) {
277 if (line.widgets[i].height) widgetsHeight += line.widgets[i].height;
278 }
279
280 if (wrapping)
281 return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th;
282 else
283 return widgetsHeight + th;
284 };
285 }
286
287 function estimateLineHeights(cm) {
288 var doc = cm.doc, est = estimateHeight(cm);
289 doc.iter(function(line) {
290 var estHeight = est(line);
291 if (estHeight != line.height) updateLineHeight(line, estHeight);
292 });
293 }
294
295 function themeChanged(cm) {
296 cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
297 cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
298 clearCaches(cm);
299 }
300
301 function guttersChanged(cm) {
302 updateGutters(cm);
303 regChange(cm);
304 setTimeout(function(){alignHorizontally(cm);}, 20);
305 }
306
307 // Rebuild the gutter elements, ensure the margin to the left of the
308 // code matches their width.
309 function updateGutters(cm) {
310 var gutters = cm.display.gutters, specs = cm.options.gutters;
311 removeChildren(gutters);
312 for (var i = 0; i < specs.length; ++i) {
313 var gutterClass = specs[i];
314 var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
315 if (gutterClass == "CodeMirror-linenumbers") {
316 cm.display.lineGutter = gElt;
317 gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
318 }
319 }
320 gutters.style.display = i ? "" : "none";
321 updateGutterSpace(cm);
322 }
323
324 function updateGutterSpace(cm) {
325 var width = cm.display.gutters.offsetWidth;
326 cm.display.sizer.style.marginLeft = width + "px";
327 }
328
329 // Compute the character length of a line, taking into account
330 // collapsed ranges (see markText) that might hide parts, and join
331 // other lines onto it.
332 function lineLength(line) {
333 if (line.height == 0) return 0;
334 var len = line.text.length, merged, cur = line;
335 while (merged = collapsedSpanAtStart(cur)) {
336 var found = merged.find(0, true);
337 cur = found.from.line;
338 len += found.from.ch - found.to.ch;
339 }
340 cur = line;
341 while (merged = collapsedSpanAtEnd(cur)) {
342 var found = merged.find(0, true);
343 len -= cur.text.length - found.from.ch;
344 cur = found.to.line;
345 len += cur.text.length - found.to.ch;
346 }
347 return len;
348 }
349
350 // Find the longest line in the document.
351 function findMaxLine(cm) {
352 var d = cm.display, doc = cm.doc;
353 d.maxLine = getLine(doc, doc.first);
354 d.maxLineLength = lineLength(d.maxLine);
355 d.maxLineChanged = true;
356 doc.iter(function(line) {
357 var len = lineLength(line);
358 if (len > d.maxLineLength) {
359 d.maxLineLength = len;
360 d.maxLine = line;
361 }
362 });
363 }
364
365 // Make sure the gutters options contains the element
366 // "CodeMirror-linenumbers" when the lineNumbers option is true.
367 function setGuttersForLineNumbers(options) {
368 var found = indexOf(options.gutters, "CodeMirror-linenumbers");
369 if (found == -1 && options.lineNumbers) {
370 options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
371 } else if (found > -1 && !options.lineNumbers) {
372 options.gutters = options.gutters.slice(0);
373 options.gutters.splice(found, 1);
374 }
375 }
376
377 // SCROLLBARS
378
379 // Prepare DOM reads needed to update the scrollbars. Done in one
380 // shot to minimize update/measure roundtrips.
381 function measureForScrollbars(cm) {
382 var d = cm.display, gutterW = d.gutters.offsetWidth;
383 var docH = Math.round(cm.doc.height + paddingVert(cm.display));
384 return {
385 clientHeight: d.scroller.clientHeight,
386 viewHeight: d.wrapper.clientHeight,
387 scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
388 viewWidth: d.wrapper.clientWidth,
389 barLeft: cm.options.fixedGutter ? gutterW : 0,
390 docHeight: docH,
391 scrollHeight: docH + scrollGap(cm) + d.barHeight,
392 nativeBarWidth: d.nativeBarWidth,
393 gutterWidth: gutterW
394 };
395 }
396
397 function NativeScrollbars(place, scroll, cm) {
398 this.cm = cm;
399 var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
400 var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
401 place(vert); place(horiz);
402
403 on(vert, "scroll", function() {
404 if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
405 });
406 on(horiz, "scroll", function() {
407 if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
408 });
409
410 this.checkedOverlay = false;
411 // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
412 if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
413 }
414
415 NativeScrollbars.prototype = copyObj({
416 update: function(measure) {
417 var needsH = measure.scrollWidth > measure.clientWidth + 1;
418 var needsV = measure.scrollHeight > measure.clientHeight + 1;
419 var sWidth = measure.nativeBarWidth;
420
421 if (needsV) {
422 this.vert.style.display = "block";
423 this.vert.style.bottom = needsH ? sWidth + "px" : "0";
424 var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
425 // A bug in IE8 can cause this value to be negative, so guard it.
426 this.vert.firstChild.style.height =
427 Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
428 } else {
429 this.vert.style.display = "";
430 this.vert.firstChild.style.height = "0";
431 }
432
433 if (needsH) {
434 this.horiz.style.display = "block";
435 this.horiz.style.right = needsV ? sWidth + "px" : "0";
436 this.horiz.style.left = measure.barLeft + "px";
437 var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
438 this.horiz.firstChild.style.width =
439 (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
440 } else {
441 this.horiz.style.display = "";
442 this.horiz.firstChild.style.width = "0";
443 }
444
445 if (!this.checkedOverlay && measure.clientHeight > 0) {
446 if (sWidth == 0) this.overlayHack();
447 this.checkedOverlay = true;
448 }
449
450 return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
451 },
452 setScrollLeft: function(pos) {
453 if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
454 },
455 setScrollTop: function(pos) {
456 if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
457 },
458 overlayHack: function() {
459 var w = mac && !mac_geMountainLion ? "12px" : "18px";
460 this.horiz.style.minHeight = this.vert.style.minWidth = w;
461 var self = this;
462 var barMouseDown = function(e) {
463 if (e_target(e) != self.vert && e_target(e) != self.horiz)
464 operation(self.cm, onMouseDown)(e);
465 };
466 on(this.vert, "mousedown", barMouseDown);
467 on(this.horiz, "mousedown", barMouseDown);
468 },
469 clear: function() {
470 var parent = this.horiz.parentNode;
471 parent.removeChild(this.horiz);
472 parent.removeChild(this.vert);
473 }
474 }, NativeScrollbars.prototype);
475
476 function NullScrollbars() {}
477
478 NullScrollbars.prototype = copyObj({
479 update: function() { return {bottom: 0, right: 0}; },
480 setScrollLeft: function() {},
481 setScrollTop: function() {},
482 clear: function() {}
483 }, NullScrollbars.prototype);
484
485 CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
486
487 function initScrollbars(cm) {
488 if (cm.display.scrollbars) {
489 cm.display.scrollbars.clear();
490 if (cm.display.scrollbars.addClass)
491 rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
492 }
493
494 cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
495 cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
496 // Prevent clicks in the scrollbars from killing focus
497 on(node, "mousedown", function() {
498 if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
499 });
500 node.setAttribute("cm-not-content", "true");
501 }, function(pos, axis) {
502 if (axis == "horizontal") setScrollLeft(cm, pos);
503 else setScrollTop(cm, pos);
504 }, cm);
505 if (cm.display.scrollbars.addClass)
506 addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
507 }
508
509 function updateScrollbars(cm, measure) {
510 if (!measure) measure = measureForScrollbars(cm);
511 var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
512 updateScrollbarsInner(cm, measure);
513 for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
514 if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
515 updateHeightsInViewport(cm);
516 updateScrollbarsInner(cm, measureForScrollbars(cm));
517 startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
518 }
519 }
520
521 // Re-synchronize the fake scrollbars with the actual size of the
522 // content.
523 function updateScrollbarsInner(cm, measure) {
524 var d = cm.display;
525 var sizes = d.scrollbars.update(measure);
526
527 d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
528 d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
529
530 if (sizes.right && sizes.bottom) {
531 d.scrollbarFiller.style.display = "block";
532 d.scrollbarFiller.style.height = sizes.bottom + "px";
533 d.scrollbarFiller.style.width = sizes.right + "px";
534 } else d.scrollbarFiller.style.display = "";
535 if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
536 d.gutterFiller.style.display = "block";
537 d.gutterFiller.style.height = sizes.bottom + "px";
538 d.gutterFiller.style.width = measure.gutterWidth + "px";
539 } else d.gutterFiller.style.display = "";
540 }
541
542 // Compute the lines that are visible in a given viewport (defaults
543 // the the current scroll position). viewport may contain top,
544 // height, and ensure (see op.scrollToPos) properties.
545 function visibleLines(display, doc, viewport) {
546 var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
547 top = Math.floor(top - paddingTop(display));
548 var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
549
550 var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
551 // Ensure is a {from: {line, ch}, to: {line, ch}} object, and
552 // forces those lines into the viewport (if possible).
553 if (viewport && viewport.ensure) {
554 var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
555 if (ensureFrom < from) {
556 from = ensureFrom;
557 to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
558 } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
559 from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
560 to = ensureTo;
561 }
562 }
563 return {from: from, to: Math.max(to, from + 1)};
564 }
565
566 // LINE NUMBERS
567
568 // Re-align line numbers and gutter marks to compensate for
569 // horizontal scrolling.
570 function alignHorizontally(cm) {
571 var display = cm.display, view = display.view;
572 if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
573 var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
574 var gutterW = display.gutters.offsetWidth, left = comp + "px";
575 for (var i = 0; i < view.length; i++) if (!view[i].hidden) {
576 if (cm.options.fixedGutter && view[i].gutter)
577 view[i].gutter.style.left = left;
578 var align = view[i].alignable;
579 if (align) for (var j = 0; j < align.length; j++)
580 align[j].style.left = left;
581 }
582 if (cm.options.fixedGutter)
583 display.gutters.style.left = (comp + gutterW) + "px";
584 }
585
586 // Used to ensure that the line number gutter is still the right
587 // size for the current document size. Returns true when an update
588 // is needed.
589 function maybeUpdateLineNumberWidth(cm) {
590 if (!cm.options.lineNumbers) return false;
591 var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
592 if (last.length != display.lineNumChars) {
593 var test = display.measure.appendChild(elt("div", [elt("div", last)],
594 "CodeMirror-linenumber CodeMirror-gutter-elt"));
595 var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
596 display.lineGutter.style.width = "";
597 display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
598 display.lineNumWidth = display.lineNumInnerWidth + padding;
599 display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
600 display.lineGutter.style.width = display.lineNumWidth + "px";
601 updateGutterSpace(cm);
602 return true;
603 }
604 return false;
605 }
606
607 function lineNumberFor(options, i) {
608 return String(options.lineNumberFormatter(i + options.firstLineNumber));
609 }
610
611 // Computes display.scroller.scrollLeft + display.gutters.offsetWidth,
612 // but using getBoundingClientRect to get a sub-pixel-accurate
613 // result.
614 function compensateForHScroll(display) {
615 return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
616 }
617
618 // DISPLAY DRAWING
619
620 function DisplayUpdate(cm, viewport, force) {
621 var display = cm.display;
622
623 this.viewport = viewport;
624 // Store some values that we'll need later (but don't want to force a relayout for)
625 this.visible = visibleLines(display, cm.doc, viewport);
626 this.editorIsHidden = !display.wrapper.offsetWidth;
627 this.wrapperHeight = display.wrapper.clientHeight;
628 this.wrapperWidth = display.wrapper.clientWidth;
629 this.oldDisplayWidth = displayWidth(cm);
630 this.force = force;
631 this.dims = getDimensions(cm);
632 this.events = [];
633 }
634
635 DisplayUpdate.prototype.signal = function(emitter, type) {
636 if (hasHandler(emitter, type))
637 this.events.push(arguments);
638 };
639 DisplayUpdate.prototype.finish = function() {
640 for (var i = 0; i < this.events.length; i++)
641 signal.apply(null, this.events[i]);
642 };
643
644 function maybeClipScrollbars(cm) {
645 var display = cm.display;
646 if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
647 display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
648 display.heightForcer.style.height = scrollGap(cm) + "px";
649 display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
650 display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
651 display.scrollbarsClipped = true;
652 }
653 }
654
655 // Does the actual updating of the line display. Bails out
656 // (returning false) when there is nothing to be done and forced is
657 // false.
658 function updateDisplayIfNeeded(cm, update) {
659 var display = cm.display, doc = cm.doc;
660
661 if (update.editorIsHidden) {
662 resetView(cm);
663 return false;
664 }
665
666 // Bail out if the visible area is already rendered and nothing changed.
667 if (!update.force &&
668 update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
669 (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
670 display.renderedView == display.view && countDirtyView(cm) == 0)
671 return false;
672
673 if (maybeUpdateLineNumberWidth(cm)) {
674 resetView(cm);
675 update.dims = getDimensions(cm);
676 }
677
678 // Compute a suitable new viewport (from & to)
679 var end = doc.first + doc.size;
680 var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
681 var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
682 if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
683 if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
684 if (sawCollapsedSpans) {
685 from = visualLineNo(cm.doc, from);
686 to = visualLineEndNo(cm.doc, to);
687 }
688
689 var different = from != display.viewFrom || to != display.viewTo ||
690 display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
691 adjustView(cm, from, to);
692
693 display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
694 // Position the mover div to align with the current scroll position
695 cm.display.mover.style.top = display.viewOffset + "px";
696
697 var toUpdate = countDirtyView(cm);
698 if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
699 (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
700 return false;
701
702 // For big changes, we hide the enclosing element during the
703 // update, since that speeds up the operations on most browsers.
704 var focused = activeElt();
705 if (toUpdate > 4) display.lineDiv.style.display = "none";
706 patchDisplay(cm, display.updateLineNumbers, update.dims);
707 if (toUpdate > 4) display.lineDiv.style.display = "";
708 display.renderedView = display.view;
709 // There might have been a widget with a focused element that got
710 // hidden or updated, if so re-focus it.
711 if (focused && activeElt() != focused && focused.offsetHeight) focused.focus();
712
713 // Prevent selection and cursors from interfering with the scroll
714 // width and height.
715 removeChildren(display.cursorDiv);
716 removeChildren(display.selectionDiv);
717 display.gutters.style.height = 0;
718
719 if (different) {
720 display.lastWrapHeight = update.wrapperHeight;
721 display.lastWrapWidth = update.wrapperWidth;
722 startWorker(cm, 400);
723 }
724
725 display.updateLineNumbers = null;
726
727 return true;
728 }
729
730 function postUpdateDisplay(cm, update) {
731 var force = update.force, viewport = update.viewport;
732 for (var first = true;; first = false) {
733 if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) {
734 force = true;
735 } else {
736 force = false;
737 // Clip forced viewport to actual scrollable area.
738 if (viewport && viewport.top != null)
739 viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
740 // Updated line heights might result in the drawn area not
741 // actually covering the viewport. Keep looping until it does.
742 update.visible = visibleLines(cm.display, cm.doc, viewport);
743 if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
744 break;
745 }
746 if (!updateDisplayIfNeeded(cm, update)) break;
747 updateHeightsInViewport(cm);
748 var barMeasure = measureForScrollbars(cm);
749 updateSelection(cm);
750 setDocumentHeight(cm, barMeasure);
751 updateScrollbars(cm, barMeasure);
752 }
753
754 update.signal(cm, "update", cm);
755 if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
756 update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
757 cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
758 }
759 }
760
761 function updateDisplaySimple(cm, viewport) {
762 var update = new DisplayUpdate(cm, viewport);
763 if (updateDisplayIfNeeded(cm, update)) {
764 updateHeightsInViewport(cm);
765 postUpdateDisplay(cm, update);
766 var barMeasure = measureForScrollbars(cm);
767 updateSelection(cm);
768 setDocumentHeight(cm, barMeasure);
769 updateScrollbars(cm, barMeasure);
770 update.finish();
771 }
772 }
773
774 function setDocumentHeight(cm, measure) {
775 cm.display.sizer.style.minHeight = measure.docHeight + "px";
776 var total = measure.docHeight + cm.display.barHeight;
777 cm.display.heightForcer.style.top = total + "px";
778 cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px";
779 }
780
781 // Read the actual heights of the rendered lines, and update their
782 // stored heights to match.
783 function updateHeightsInViewport(cm) {
784 var display = cm.display;
785 var prevBottom = display.lineDiv.offsetTop;
786 for (var i = 0; i < display.view.length; i++) {
787 var cur = display.view[i], height;
788 if (cur.hidden) continue;
789 if (ie && ie_version < 8) {
790 var bot = cur.node.offsetTop + cur.node.offsetHeight;
791 height = bot - prevBottom;
792 prevBottom = bot;
793 } else {
794 var box = cur.node.getBoundingClientRect();
795 height = box.bottom - box.top;
796 }
797 var diff = cur.line.height - height;
798 if (height < 2) height = textHeight(display);
799 if (diff > .001 || diff < -.001) {
800 updateLineHeight(cur.line, height);
801 updateWidgetHeight(cur.line);
802 if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
803 updateWidgetHeight(cur.rest[j]);
804 }
805 }
806 }
807
808 // Read and store the height of line widgets associated with the
809 // given line.
810 function updateWidgetHeight(line) {
811 if (line.widgets) for (var i = 0; i < line.widgets.length; ++i)
812 line.widgets[i].height = line.widgets[i].node.offsetHeight;
813 }
814
815 // Do a bulk-read of the DOM positions and sizes needed to draw the
816 // view, so that we don't interleave reading and writing to the DOM.
817 function getDimensions(cm) {
818 var d = cm.display, left = {}, width = {};
819 var gutterLeft = d.gutters.clientLeft;
820 for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
821 left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
822 width[cm.options.gutters[i]] = n.clientWidth;
823 }
824 return {fixedPos: compensateForHScroll(d),
825 gutterTotalWidth: d.gutters.offsetWidth,
826 gutterLeft: left,
827 gutterWidth: width,
828 wrapperWidth: d.wrapper.clientWidth};
829 }
830
831 // Sync the actual display DOM structure with display.view, removing
832 // nodes for lines that are no longer in view, and creating the ones
833 // that are not there yet, and updating the ones that are out of
834 // date.
835 function patchDisplay(cm, updateNumbersFrom, dims) {
836 var display = cm.display, lineNumbers = cm.options.lineNumbers;
837 var container = display.lineDiv, cur = container.firstChild;
838
839 function rm(node) {
840 var next = node.nextSibling;
841 // Works around a throw-scroll bug in OS X Webkit
842 if (webkit && mac && cm.display.currentWheelTarget == node)
843 node.style.display = "none";
844 else
845 node.parentNode.removeChild(node);
846 return next;
847 }
848
849 var view = display.view, lineN = display.viewFrom;
850 // Loop over the elements in the view, syncing cur (the DOM nodes
851 // in display.lineDiv) with the view as we go.
852 for (var i = 0; i < view.length; i++) {
853 var lineView = view[i];
854 if (lineView.hidden) {
855 } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
856 var node = buildLineElement(cm, lineView, lineN, dims);
857 container.insertBefore(node, cur);
858 } else { // Already drawn
859 while (cur != lineView.node) cur = rm(cur);
860 var updateNumber = lineNumbers && updateNumbersFrom != null &&
861 updateNumbersFrom <= lineN && lineView.lineNumber;
862 if (lineView.changes) {
863 if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false;
864 updateLineForChanges(cm, lineView, lineN, dims);
865 }
866 if (updateNumber) {
867 removeChildren(lineView.lineNumber);
868 lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
869 }
870 cur = lineView.node.nextSibling;
871 }
872 lineN += lineView.size;
873 }
874 while (cur) cur = rm(cur);
875 }
876
877 // When an aspect of a line changes, a string is added to
878 // lineView.changes. This updates the relevant part of the line's
879 // DOM structure.
880 function updateLineForChanges(cm, lineView, lineN, dims) {
881 for (var j = 0; j < lineView.changes.length; j++) {
882 var type = lineView.changes[j];
883 if (type == "text") updateLineText(cm, lineView);
884 else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims);
885 else if (type == "class") updateLineClasses(lineView);
886 else if (type == "widget") updateLineWidgets(cm, lineView, dims);
887 }
888 lineView.changes = null;
889 }
890
891 // Lines with gutter elements, widgets or a background class need to
892 // be wrapped, and have the extra elements added to the wrapper div
893 function ensureLineWrapped(lineView) {
894 if (lineView.node == lineView.text) {
895 lineView.node = elt("div", null, null, "position: relative");
896 if (lineView.text.parentNode)
897 lineView.text.parentNode.replaceChild(lineView.node, lineView.text);
898 lineView.node.appendChild(lineView.text);
899 if (ie && ie_version < 8) lineView.node.style.zIndex = 2;
900 }
901 return lineView.node;
902 }
903
904 function updateLineBackground(lineView) {
905 var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass;
906 if (cls) cls += " CodeMirror-linebackground";
907 if (lineView.background) {
908 if (cls) lineView.background.className = cls;
909 else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; }
910 } else if (cls) {
911 var wrap = ensureLineWrapped(lineView);
912 lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild);
913 }
914 }
915
916 // Wrapper around buildLineContent which will reuse the structure
917 // in display.externalMeasured when possible.
918 function getLineContent(cm, lineView) {
919 var ext = cm.display.externalMeasured;
920 if (ext && ext.line == lineView.line) {
921 cm.display.externalMeasured = null;
922 lineView.measure = ext.measure;
923 return ext.built;
924 }
925 return buildLineContent(cm, lineView);
926 }
927
928 // Redraw the line's text. Interacts with the background and text
929 // classes because the mode may output tokens that influence these
930 // classes.
931 function updateLineText(cm, lineView) {
932 var cls = lineView.text.className;
933 var built = getLineContent(cm, lineView);
934 if (lineView.text == lineView.node) lineView.node = built.pre;
935 lineView.text.parentNode.replaceChild(built.pre, lineView.text);
936 lineView.text = built.pre;
937 if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) {
938 lineView.bgClass = built.bgClass;
939 lineView.textClass = built.textClass;
940 updateLineClasses(lineView);
941 } else if (cls) {
942 lineView.text.className = cls;
943 }
944 }
945
946 function updateLineClasses(lineView) {
947 updateLineBackground(lineView);
948 if (lineView.line.wrapClass)
949 ensureLineWrapped(lineView).className = lineView.line.wrapClass;
950 else if (lineView.node != lineView.text)
951 lineView.node.className = "";
952 var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass;
953 lineView.text.className = textClass || "";
954 }
955
956 function updateLineGutter(cm, lineView, lineN, dims) {
957 if (lineView.gutter) {
958 lineView.node.removeChild(lineView.gutter);
959 lineView.gutter = null;
960 }
961 var markers = lineView.line.gutterMarkers;
962 if (cm.options.lineNumbers || markers) {
963 var wrap = ensureLineWrapped(lineView);
964 var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
965 (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
966 "px; width: " + dims.gutterTotalWidth + "px");
967 cm.display.input.setUneditable(gutterWrap);
968 wrap.insertBefore(gutterWrap, lineView.text);
969 if (lineView.line.gutterClass)
970 gutterWrap.className += " " + lineView.line.gutterClass;
971 if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
972 lineView.lineNumber = gutterWrap.appendChild(
973 elt("div", lineNumberFor(cm.options, lineN),
974 "CodeMirror-linenumber CodeMirror-gutter-elt",
975 "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
976 + cm.display.lineNumInnerWidth + "px"));
977 if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) {
978 var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
979 if (found)
980 gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
981 dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
982 }
983 }
984 }
985
986 function updateLineWidgets(cm, lineView, dims) {
987 if (lineView.alignable) lineView.alignable = null;
988 for (var node = lineView.node.firstChild, next; node; node = next) {
989 var next = node.nextSibling;
990 if (node.className == "CodeMirror-linewidget")
991 lineView.node.removeChild(node);
992 }
993 insertLineWidgets(cm, lineView, dims);
994 }
995
996 // Build a line's DOM representation from scratch
997 function buildLineElement(cm, lineView, lineN, dims) {
998 var built = getLineContent(cm, lineView);
999 lineView.text = lineView.node = built.pre;
1000 if (built.bgClass) lineView.bgClass = built.bgClass;
1001 if (built.textClass) lineView.textClass = built.textClass;
1002
1003 updateLineClasses(lineView);
1004 updateLineGutter(cm, lineView, lineN, dims);
1005 insertLineWidgets(cm, lineView, dims);
1006 return lineView.node;
1007 }
1008
1009 // A lineView may contain multiple logical lines (when merged by
1010 // collapsed spans). The widgets for all of them need to be drawn.
1011 function insertLineWidgets(cm, lineView, dims) {
1012 insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
1013 if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
1014 insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
1015 }
1016
1017 function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) {
1018 if (!line.widgets) return;
1019 var wrap = ensureLineWrapped(lineView);
1020 for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
1021 var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
1022 if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true");
1023 positionLineWidget(widget, node, lineView, dims);
1024 cm.display.input.setUneditable(node);
1025 if (allowAbove && widget.above)
1026 wrap.insertBefore(node, lineView.gutter || lineView.text);
1027 else
1028 wrap.appendChild(node);
1029 signalLater(widget, "redraw");
1030 }
1031 }
1032
1033 function positionLineWidget(widget, node, lineView, dims) {
1034 if (widget.noHScroll) {
1035 (lineView.alignable || (lineView.alignable = [])).push(node);
1036 var width = dims.wrapperWidth;
1037 node.style.left = dims.fixedPos + "px";
1038 if (!widget.coverGutter) {
1039 width -= dims.gutterTotalWidth;
1040 node.style.paddingLeft = dims.gutterTotalWidth + "px";
1041 }
1042 node.style.width = width + "px";
1043 }
1044 if (widget.coverGutter) {
1045 node.style.zIndex = 5;
1046 node.style.position = "relative";
1047 if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
1048 }
1049 }
1050
1051 // POSITION OBJECT
1052
1053 // A Pos instance represents a position within the text.
1054 var Pos = CodeMirror.Pos = function(line, ch) {
1055 if (!(this instanceof Pos)) return new Pos(line, ch);
1056 this.line = line; this.ch = ch;
1057 };
1058
1059 // Compare two positions, return 0 if they are the same, a negative
1060 // number when a is less, and a positive number otherwise.
1061 var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; };
1062
1063 function copyPos(x) {return Pos(x.line, x.ch);}
1064 function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; }
1065 function minPos(a, b) { return cmp(a, b) < 0 ? a : b; }
1066
1067 // INPUT HANDLING
1068
1069 function ensureFocus(cm) {
1070 if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); }
1071 }
1072
1073 function isReadOnly(cm) {
1074 return cm.options.readOnly || cm.doc.cantEdit;
1075 }
1076
1077 // This will be set to an array of strings when copying, so that,
1078 // when pasting, we know what kind of selections the copied text
1079 // was made out of.
1080 var lastCopied = null;
1081
1082 function applyTextInput(cm, inserted, deleted, sel, origin) {
1083 var doc = cm.doc;
1084 cm.display.shift = false;
1085 if (!sel) sel = doc.sel;
1086
1087 var textLines = splitLines(inserted), multiPaste = null;
1088 // When pasing N lines into N selections, insert one line per selection
1089 if (cm.state.pasteIncoming && sel.ranges.length > 1) {
1090 if (lastCopied && lastCopied.join("\n") == inserted)
1091 multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines);
1092 else if (textLines.length == sel.ranges.length)
1093 multiPaste = map(textLines, function(l) { return [l]; });
1094 }
1095
1096 // Normal behavior is to insert the new text into every selection
1097 for (var i = sel.ranges.length - 1; i >= 0; i--) {
1098 var range = sel.ranges[i];
1099 var from = range.from(), to = range.to();
1100 if (range.empty()) {
1101 if (deleted && deleted > 0) // Handle deletion
1102 from = Pos(from.line, from.ch - deleted);
1103 else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite
1104 to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
1105 }
1106 var updateInput = cm.curOp.updateInput;
1107 var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
1108 origin: origin || (cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input")};
1109 makeChange(cm.doc, changeEvent);
1110 signalLater(cm, "inputRead", cm, changeEvent);
1111 // When an 'electric' character is inserted, immediately trigger a reindent
1112 if (inserted && !cm.state.pasteIncoming && cm.options.electricChars &&
1113 cm.options.smartIndent && range.head.ch < 100 &&
1114 (!i || sel.ranges[i - 1].head.line != range.head.line)) {
1115 var mode = cm.getModeAt(range.head);
1116 var end = changeEnd(changeEvent);
1117 var indented = false;
1118 if (mode.electricChars) {
1119 for (var j = 0; j < mode.electricChars.length; j++)
1120 if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {
1121 indented = indentLine(cm, end.line, "smart");
1122 break;
1123 }
1124 } else if (mode.electricInput) {
1125 if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch)))
1126 indented = indentLine(cm, end.line, "smart");
1127 }
1128 if (indented) signalLater(cm, "electricInput", cm, end.line);
1129 }
1130 }
1131 ensureCursorVisible(cm);
1132 cm.curOp.updateInput = updateInput;
1133 cm.curOp.typing = true;
1134 cm.state.pasteIncoming = cm.state.cutIncoming = false;
1135 }
1136
1137 function copyableRanges(cm) {
1138 var text = [], ranges = [];
1139 for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
1140 var line = cm.doc.sel.ranges[i].head.line;
1141 var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
1142 ranges.push(lineRange);
1143 text.push(cm.getRange(lineRange.anchor, lineRange.head));
1144 }
1145 return {text: text, ranges: ranges};
1146 }
1147
1148 function disableBrowserMagic(field) {
1149 field.setAttribute("autocorrect", "off");
1150 field.setAttribute("autocapitalize", "off");
1151 field.setAttribute("spellcheck", "false");
1152 }
1153
1154 // TEXTAREA INPUT STYLE
1155
1156 function TextareaInput(cm) {
1157 this.cm = cm;
1158 // See input.poll and input.reset
1159 this.prevInput = "";
1160
1161 // Flag that indicates whether we expect input to appear real soon
1162 // now (after some event like 'keypress' or 'input') and are
1163 // polling intensively.
1164 this.pollingFast = false;
1165 // Self-resetting timeout for the poller
1166 this.polling = new Delayed();
1167 // Tracks when input.reset has punted to just putting a short
1168 // string into the textarea instead of the full selection.
1169 this.inaccurateSelection = false;
1170 // Used to work around IE issue with selection being forgotten when focus moves away from textarea
1171 this.hasSelection = false;
1172 this.composing = null;
1173 };
1174
1175 function hiddenTextarea() {
1176 var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none");
1177 var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
1178 // The textarea is kept positioned near the cursor to prevent the
1179 // fact that it'll be scrolled into view on input from scrolling
1180 // our fake cursor out of view. On webkit, when wrap=off, paste is
1181 // very slow. So make the area wide instead.
1182 if (webkit) te.style.width = "1000px";
1183 else te.setAttribute("wrap", "off");
1184 // If border: 0; -- iOS fails to open keyboard (issue #1287)
1185 if (ios) te.style.border = "1px solid black";
1186 disableBrowserMagic(te);
1187 return div;
1188 }
1189
1190 TextareaInput.prototype = copyObj({
1191 init: function(display) {
1192 var input = this, cm = this.cm;
1193
1194 // Wraps and hides input textarea
1195 var div = this.wrapper = hiddenTextarea();
1196 // The semihidden textarea that is focused when the editor is
1197 // focused, and receives input.
1198 var te = this.textarea = div.firstChild;
1199 display.wrapper.insertBefore(div, display.wrapper.firstChild);
1200
1201 // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
1202 if (ios) te.style.width = "0px";
1203
1204 on(te, "input", function() {
1205 if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null;
1206 input.poll();
1207 });
1208
1209 on(te, "paste", function() {
1210 // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206
1211 // Add a char to the end of textarea before paste occur so that
1212 // selection doesn't span to the end of textarea.
1213 if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) {
1214 var start = te.selectionStart, end = te.selectionEnd;
1215 te.value += "$";
1216 // The selection end needs to be set before the start, otherwise there
1217 // can be an intermediate non-empty selection between the two, which
1218 // can override the middle-click paste buffer on linux and cause the
1219 // wrong thing to get pasted.
1220 te.selectionEnd = end;
1221 te.selectionStart = start;
1222 cm.state.fakedLastChar = true;
1223 }
1224 cm.state.pasteIncoming = true;
1225 input.fastPoll();
1226 });
1227
1228 function prepareCopyCut(e) {
1229 if (cm.somethingSelected()) {
1230 lastCopied = cm.getSelections();
1231 if (input.inaccurateSelection) {
1232 input.prevInput = "";
1233 input.inaccurateSelection = false;
1234 te.value = lastCopied.join("\n");
1235 selectInput(te);
1236 }
1237 } else if (!cm.options.lineWiseCopyCut) {
1238 return;
1239 } else {
1240 var ranges = copyableRanges(cm);
1241 lastCopied = ranges.text;
1242 if (e.type == "cut") {
1243 cm.setSelections(ranges.ranges, null, sel_dontScroll);
1244 } else {
1245 input.prevInput = "";
1246 te.value = ranges.text.join("\n");
1247 selectInput(te);
1248 }
1249 }
1250 if (e.type == "cut") cm.state.cutIncoming = true;
1251 }
1252 on(te, "cut", prepareCopyCut);
1253 on(te, "copy", prepareCopyCut);
1254
1255 on(display.scroller, "paste", function(e) {
1256 if (eventInWidget(display, e)) return;
1257 cm.state.pasteIncoming = true;
1258 input.focus();
1259 });
1260
1261 // Prevent normal selection in the editor (we handle our own)
1262 on(display.lineSpace, "selectstart", function(e) {
1263 if (!eventInWidget(display, e)) e_preventDefault(e);
1264 });
1265
1266 on(te, "compositionstart", function() {
1267 var start = cm.getCursor("from");
1268 input.composing = {
1269 start: start,
1270 range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"})
1271 };
1272 });
1273 on(te, "compositionend", function() {
1274 if (input.composing) {
1275 input.poll();
1276 input.composing.range.clear();
1277 input.composing = null;
1278 }
1279 });
1280 },
1281
1282 prepareSelection: function() {
1283 // Redraw the selection and/or cursor
1284 var cm = this.cm, display = cm.display, doc = cm.doc;
1285 var result = prepareSelection(cm);
1286
1287 // Move the hidden textarea near the cursor to prevent scrolling artifacts
1288 if (cm.options.moveInputWithCursor) {
1289 var headPos = cursorCoords(cm, doc.sel.primary().head, "div");
1290 var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect();
1291 result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
1292 headPos.top + lineOff.top - wrapOff.top));
1293 result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
1294 headPos.left + lineOff.left - wrapOff.left));
1295 }
1296
1297 return result;
1298 },
1299
1300 showSelection: function(drawn) {
1301 var cm = this.cm, display = cm.display;
1302 removeChildrenAndAdd(display.cursorDiv, drawn.cursors);
1303 removeChildrenAndAdd(display.selectionDiv, drawn.selection);
1304 if (drawn.teTop != null) {
1305 this.wrapper.style.top = drawn.teTop + "px";
1306 this.wrapper.style.left = drawn.teLeft + "px";
1307 }
1308 },
1309
1310 // Reset the input to correspond to the selection (or to be empty,
1311 // when not typing and nothing is selected)
1312 reset: function(typing) {
1313 if (this.contextMenuPending) return;
1314 var minimal, selected, cm = this.cm, doc = cm.doc;
1315 if (cm.somethingSelected()) {
1316 this.prevInput = "";
1317 var range = doc.sel.primary();
1318 minimal = hasCopyEvent &&
1319 (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
1320 var content = minimal ? "-" : selected || cm.getSelection();
1321 this.textarea.value = content;
1322 if (cm.state.focused) selectInput(this.textarea);
1323 if (ie && ie_version >= 9) this.hasSelection = content;
1324 } else if (!typing) {
1325 this.prevInput = this.textarea.value = "";
1326 if (ie && ie_version >= 9) this.hasSelection = null;
1327 }
1328 this.inaccurateSelection = minimal;
1329 },
1330
1331 getField: function() { return this.textarea; },
1332
1333 supportsTouch: function() { return false; },
1334
1335 focus: function() {
1336 if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) {
1337 try { this.textarea.focus(); }
1338 catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM
1339 }
1340 },
1341
1342 blur: function() { this.textarea.blur(); },
1343
1344 resetPosition: function() {
1345 this.wrapper.style.top = this.wrapper.style.left = 0;
1346 },
1347
1348 receivedFocus: function() { this.slowPoll(); },
1349
1350 // Poll for input changes, using the normal rate of polling. This
1351 // runs as long as the editor is focused.
1352 slowPoll: function() {
1353 var input = this;
1354 if (input.pollingFast) return;
1355 input.polling.set(this.cm.options.pollInterval, function() {
1356 input.poll();
1357 if (input.cm.state.focused) input.slowPoll();
1358 });
1359 },
1360
1361 // When an event has just come in that is likely to add or change
1362 // something in the input textarea, we poll faster, to ensure that
1363 // the change appears on the screen quickly.
1364 fastPoll: function() {
1365 var missed = false, input = this;
1366 input.pollingFast = true;
1367 function p() {
1368 var changed = input.poll();
1369 if (!changed && !missed) {missed = true; input.polling.set(60, p);}
1370 else {input.pollingFast = false; input.slowPoll();}
1371 }
1372 input.polling.set(20, p);
1373 },
1374
1375 // Read input from the textarea, and update the document to match.
1376 // When something is selected, it is present in the textarea, and
1377 // selected (unless it is huge, in which case a placeholder is
1378 // used). When nothing is selected, the cursor sits after previously
1379 // seen text (can be empty), which is stored in prevInput (we must
1380 // not reset the textarea when typing, because that breaks IME).
1381 poll: function() {
1382 var cm = this.cm, input = this.textarea, prevInput = this.prevInput;
1383 // Since this is called a *lot*, try to bail out as cheaply as
1384 // possible when it is clear that nothing happened. hasSelection
1385 // will be the case when there is a lot of text in the textarea,
1386 // in which case reading its value would be expensive.
1387 if (!cm.state.focused || (hasSelection(input) && !prevInput) ||
1388 isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq)
1389 return false;
1390 // See paste handler for more on the fakedLastChar kludge
1391 if (cm.state.pasteIncoming && cm.state.fakedLastChar) {
1392 input.value = input.value.substring(0, input.value.length - 1);
1393 cm.state.fakedLastChar = false;
1394 }
1395 var text = input.value;
1396 // If nothing changed, bail.
1397 if (text == prevInput && !cm.somethingSelected()) return false;
1398 // Work around nonsensical selection resetting in IE9/10, and
1399 // inexplicable appearance of private area unicode characters on
1400 // some key combos in Mac (#2689).
1401 if (ie && ie_version >= 9 && this.hasSelection === text ||
1402 mac && /[\uf700-\uf7ff]/.test(text)) {
1403 cm.display.input.reset();
1404 return false;
1405 }
1406
1407 if (cm.doc.sel == cm.display.selForContextMenu) {
1408 var first = text.charCodeAt(0);
1409 if (first == 0x200b && !prevInput) prevInput = "\u200b";
1410 if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); }
1411 }
1412 // Find the part of the input that is actually new
1413 var same = 0, l = Math.min(prevInput.length, text.length);
1414 while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
1415
1416 var self = this;
1417 runInOp(cm, function() {
1418 applyTextInput(cm, text.slice(same), prevInput.length - same,
1419 null, self.composing ? "*compose" : null);
1420
1421 // Don't leave long text in the textarea, since it makes further polling slow
1422 if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = "";
1423 else self.prevInput = text;
1424
1425 if (self.composing) {
1426 self.composing.range.clear();
1427 self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"),
1428 {className: "CodeMirror-composing"});
1429 }
1430 });
1431 return true;
1432 },
1433
1434 ensurePolled: function() {
1435 if (this.pollingFast && this.poll()) this.pollingFast = false;
1436 },
1437
1438 onKeyPress: function() {
1439 if (ie && ie_version >= 9) this.hasSelection = null;
1440 this.fastPoll();
1441 },
1442
1443 onContextMenu: function(e) {
1444 var input = this, cm = input.cm, display = cm.display, te = input.textarea;
1445 var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
1446 if (!pos || presto) return; // Opera is difficult.
1447
1448 // Reset the current text selection only if the click is done outside of the selection
1449 // and 'resetSelectionOnContextMenu' option is true.
1450 var reset = cm.options.resetSelectionOnContextMenu;
1451 if (reset && cm.doc.sel.contains(pos) == -1)
1452 operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll);
1453
1454 var oldCSS = te.style.cssText;
1455 input.wrapper.style.position = "absolute";
1456 te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
1457 "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " +
1458 (ie ? "rgba(255, 255, 255, .05)" : "transparent") +
1459 "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
1460 if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
1461 display.input.focus();
1462 if (webkit) window.scrollTo(null, oldScrollY);
1463 display.input.reset();
1464 // Adds "Select all" to context menu in FF
1465 if (!cm.somethingSelected()) te.value = input.prevInput = " ";
1466 input.contextMenuPending = true;
1467 display.selForContextMenu = cm.doc.sel;
1468 clearTimeout(display.detectingSelectAll);
1469
1470 // Select-all will be greyed out if there's nothing to select, so
1471 // this adds a zero-width space so that we can later check whether
1472 // it got selected.
1473 function prepareSelectAllHack() {
1474 if (te.selectionStart != null) {
1475 var selected = cm.somethingSelected();
1476 var extval = "\u200b" + (selected ? te.value : "");
1477 te.value = "\u21da"; // Used to catch context-menu undo
1478 te.value = extval;
1479 input.prevInput = selected ? "" : "\u200b";
1480 te.selectionStart = 1; te.selectionEnd = extval.length;
1481 // Re-set this, in case some other handler touched the
1482 // selection in the meantime.
1483 display.selForContextMenu = cm.doc.sel;
1484 }
1485 }
1486 function rehide() {
1487 input.contextMenuPending = false;
1488 input.wrapper.style.position = "relative";
1489 te.style.cssText = oldCSS;
1490 if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos);
1491
1492 // Try to detect the user choosing select-all
1493 if (te.selectionStart != null) {
1494 if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
1495 var i = 0, poll = function() {
1496 if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&
1497 te.selectionEnd > 0 && input.prevInput == "\u200b")
1498 operation(cm, commands.selectAll)(cm);
1499 else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500);
1500 else display.input.reset();
1501 };
1502 display.detectingSelectAll = setTimeout(poll, 200);
1503 }
1504 }
1505
1506 if (ie && ie_version >= 9) prepareSelectAllHack();
1507 if (captureRightClick) {
1508 e_stop(e);
1509 var mouseup = function() {
1510 off(window, "mouseup", mouseup);
1511 setTimeout(rehide, 20);
1512 };
1513 on(window, "mouseup", mouseup);
1514 } else {
1515 setTimeout(rehide, 50);
1516 }
1517 },
1518
1519 setUneditable: nothing,
1520
1521 needsContentAttribute: false
1522 }, TextareaInput.prototype);
1523
1524 // CONTENTEDITABLE INPUT STYLE
1525
1526 function ContentEditableInput(cm) {
1527 this.cm = cm;
1528 this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null;
1529 this.polling = new Delayed();
1530 this.gracePeriod = false;
1531 }
1532
1533 ContentEditableInput.prototype = copyObj({
1534 init: function(display) {
1535 var input = this, cm = input.cm;
1536 var div = input.div = display.lineDiv;
1537 div.contentEditable = "true";
1538 disableBrowserMagic(div);
1539
1540 on(div, "paste", function(e) {
1541 var pasted = e.clipboardData && e.clipboardData.getData("text/plain");
1542 if (pasted) {
1543 e.preventDefault();
1544 cm.replaceSelection(pasted, null, "paste");
1545 }
1546 });
1547
1548 on(div, "compositionstart", function(e) {
1549 var data = e.data;
1550 input.composing = {sel: cm.doc.sel, data: data, startData: data};
1551 if (!data) return;
1552 var prim = cm.doc.sel.primary();
1553 var line = cm.getLine(prim.head.line);
1554 var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length));
1555 if (found > -1 && found <= prim.head.ch)
1556 input.composing.sel = simpleSelection(Pos(prim.head.line, found),
1557 Pos(prim.head.line, found + data.length));
1558 });
1559 on(div, "compositionupdate", function(e) {
1560 input.composing.data = e.data;
1561 });
1562 on(div, "compositionend", function(e) {
1563 var ours = input.composing;
1564 if (!ours) return;
1565 if (e.data != ours.startData && !/\u200b/.test(e.data))
1566 ours.data = e.data;
1567 // Need a small delay to prevent other code (input event,
1568 // selection polling) from doing damage when fired right after
1569 // compositionend.
1570 setTimeout(function() {
1571 if (!ours.handled)
1572 input.applyComposition(ours);
1573 if (input.composing == ours)
1574 input.composing = null;
1575 }, 50);
1576 });
1577
1578 on(div, "touchstart", function() {
1579 input.forceCompositionEnd();
1580 });
1581
1582 on(div, "input", function() {
1583 if (input.composing) return;
1584 if (!input.pollContent())
1585 runInOp(input.cm, function() {regChange(cm);});
1586 });
1587
1588 function onCopyCut(e) {
1589 if (cm.somethingSelected()) {
1590 lastCopied = cm.getSelections();
1591 if (e.type == "cut") cm.replaceSelection("", null, "cut");
1592 } else if (!cm.options.lineWiseCopyCut) {
1593 return;
1594 } else {
1595 var ranges = copyableRanges(cm);
1596 lastCopied = ranges.text;
1597 if (e.type == "cut") {
1598 cm.operation(function() {
1599 cm.setSelections(ranges.ranges, 0, sel_dontScroll);
1600 cm.replaceSelection("", null, "cut");
1601 });
1602 }
1603 }
1604 // iOS exposes the clipboard API, but seems to discard content inserted into it
1605 if (e.clipboardData && !ios) {
1606 e.preventDefault();
1607 e.clipboardData.clearData();
1608 e.clipboardData.setData("text/plain", lastCopied.join("\n"));
1609 } else {
1610 // Old-fashioned briefly-focus-a-textarea hack
1611 var kludge = hiddenTextarea(), te = kludge.firstChild;
1612 cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild);
1613 te.value = lastCopied.join("\n");
1614 var hadFocus = document.activeElement;
1615 selectInput(te);
1616 setTimeout(function() {
1617 cm.display.lineSpace.removeChild(kludge);
1618 hadFocus.focus();
1619 }, 50);
1620 }
1621 }
1622 on(div, "copy", onCopyCut);
1623 on(div, "cut", onCopyCut);
1624 },
1625
1626 prepareSelection: function() {
1627 var result = prepareSelection(this.cm, false);
1628 result.focus = this.cm.state.focused;
1629 return result;
1630 },
1631
1632 showSelection: function(info) {
1633 if (!info || !this.cm.display.view.length) return;
1634 if (info.focus) this.showPrimarySelection();
1635 this.showMultipleSelections(info);
1636 },
1637
1638 showPrimarySelection: function() {
1639 var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
1640 var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
1641 var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset);
1642 if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&
1643 cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&
1644 cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)
1645 return;
1646
1647 var start = posToDOM(this.cm, prim.from());
1648 var end = posToDOM(this.cm, prim.to());
1649 if (!start && !end) return;
1650
1651 var view = this.cm.display.view;
1652 var old = sel.rangeCount && sel.getRangeAt(0);
1653 if (!start) {
1654 start = {node: view[0].measure.map[2], offset: 0};
1655 } else if (!end) { // FIXME dangerously hacky
1656 var measure = view[view.length - 1].measure;
1657 var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map;
1658 end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]};
1659 }
1660
1661 try { var rng = range(start.node, start.offset, end.offset, end.node); }
1662 catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible
1663 if (rng) {
1664 sel.removeAllRanges();
1665 sel.addRange(rng);
1666 if (old && sel.anchorNode == null) sel.addRange(old);
1667 else if (gecko) this.startGracePeriod();
1668 }
1669 this.rememberSelection();
1670 },
1671
1672 startGracePeriod: function() {
1673 var input = this;
1674 clearTimeout(this.gracePeriod);
1675 this.gracePeriod = setTimeout(function() {
1676 input.gracePeriod = false;
1677 if (input.selectionChanged())
1678 input.cm.operation(function() { input.cm.curOp.selectionChanged = true; });
1679 }, 20);
1680 },
1681
1682 showMultipleSelections: function(info) {
1683 removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors);
1684 removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection);
1685 },
1686
1687 rememberSelection: function() {
1688 var sel = window.getSelection();
1689 this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset;
1690 this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset;
1691 },
1692
1693 selectionInEditor: function() {
1694 var sel = window.getSelection();
1695 if (!sel.rangeCount) return false;
1696 var node = sel.getRangeAt(0).commonAncestorContainer;
1697 return contains(this.div, node);
1698 },
1699
1700 focus: function() {
1701 if (this.cm.options.readOnly != "nocursor") this.div.focus();
1702 },
1703 blur: function() { this.div.blur(); },
1704 getField: function() { return this.div; },
1705
1706 supportsTouch: function() { return true; },
1707
1708 receivedFocus: function() {
1709 var input = this;
1710 if (this.selectionInEditor())
1711 this.pollSelection();
1712 else
1713 runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; });
1714
1715 function poll() {
1716 if (input.cm.state.focused) {
1717 input.pollSelection();
1718 input.polling.set(input.cm.options.pollInterval, poll);
1719 }
1720 }
1721 this.polling.set(this.cm.options.pollInterval, poll);
1722 },
1723
1724 selectionChanged: function() {
1725 var sel = window.getSelection();
1726 return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
1727 sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset;
1728 },
1729
1730 pollSelection: function() {
1731 if (!this.composing && !this.gracePeriod && this.selectionChanged()) {
1732 var sel = window.getSelection(), cm = this.cm;
1733 this.rememberSelection();
1734 var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset);
1735 var head = domToPos(cm, sel.focusNode, sel.focusOffset);
1736 if (anchor && head) runInOp(cm, function() {
1737 setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll);
1738 if (anchor.bad || head.bad) cm.curOp.selectionChanged = true;
1739 });
1740 }
1741 },
1742
1743 pollContent: function() {
1744 var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary();
1745 var from = sel.from(), to = sel.to();
1746 if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
1747
1748 var fromIndex;
1749 if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {
1750 var fromLine = lineNo(display.view[0].line);
1751 var fromNode = display.view[0].node;
1752 } else {
1753 var fromLine = lineNo(display.view[fromIndex].line);
1754 var fromNode = display.view[fromIndex - 1].node.nextSibling;
1755 }
1756 var toIndex = findViewIndex(cm, to.line);
1757 if (toIndex == display.view.length - 1) {
1758 var toLine = display.viewTo - 1;
1759 var toNode = display.view[toIndex].node;
1760 } else {
1761 var toLine = lineNo(display.view[toIndex + 1].line) - 1;
1762 var toNode = display.view[toIndex + 1].node.previousSibling;
1763 }
1764
1765 var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine));
1766 var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
1767 while (newText.length > 1 && oldText.length > 1) {
1768 if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
1769 else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
1770 else break;
1771 }
1772
1773 var cutFront = 0, cutEnd = 0;
1774 var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length);
1775 while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
1776 ++cutFront;
1777 var newBot = lst(newText), oldBot = lst(oldText);
1778 var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),
1779 oldBot.length - (oldText.length == 1 ? cutFront : 0));
1780 while (cutEnd < maxCutEnd &&
1781 newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))
1782 ++cutEnd;
1783
1784 newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
1785 newText[0] = newText[0].slice(cutFront);
1786
1787 var chFrom = Pos(fromLine, cutFront);
1788 var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
1789 if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
1790 replaceRange(cm.doc, newText, chFrom, chTo, "+input");
1791 return true;
1792 }
1793 },
1794
1795 ensurePolled: function() {
1796 this.forceCompositionEnd();
1797 },
1798 reset: function() {
1799 this.forceCompositionEnd();
1800 },
1801 forceCompositionEnd: function() {
1802 if (!this.composing || this.composing.handled) return;
1803 this.applyComposition(this.composing);
1804 this.composing.handled = true;
1805 this.div.blur();
1806 this.div.focus();
1807 },
1808 applyComposition: function(composing) {
1809 if (composing.data && composing.data != composing.startData)
1810 operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
1811 },
1812
1813 setUneditable: function(node) {
1814 node.setAttribute("contenteditable", "false");
1815 },
1816
1817 onKeyPress: function(e) {
1818 e.preventDefault();
1819 operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0);
1820 },
1821
1822 onContextMenu: nothing,
1823 resetPosition: nothing,
1824
1825 needsContentAttribute: true
1826 }, ContentEditableInput.prototype);
1827
1828 function posToDOM(cm, pos) {
1829 var view = findViewForLine(cm, pos.line);
1830 if (!view || view.hidden) return null;
1831 var line = getLine(cm.doc, pos.line);
1832 var info = mapFromLineView(view, line, pos.line);
1833
1834 var order = getOrder(line), side = "left";
1835 if (order) {
1836 var partPos = getBidiPartAt(order, pos.ch);
1837 side = partPos % 2 ? "right" : "left";
1838 }
1839 var result = nodeAndOffsetInLineMap(info.map, pos.ch, "left");
1840 result.offset = result.collapse == "right" ? result.end : result.start;
1841 return result;
1842 }
1843
1844 function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
1845
1846 function domToPos(cm, node, offset) {
1847 var lineNode;
1848 if (node == cm.display.lineDiv) {
1849 lineNode = cm.display.lineDiv.childNodes[offset];
1850 if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true);
1851 node = null; offset = 0;
1852 } else {
1853 for (lineNode = node;; lineNode = lineNode.parentNode) {
1854 if (!lineNode || lineNode == cm.display.lineDiv) return null;
1855 if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
1856 }
1857 }
1858 for (var i = 0; i < cm.display.view.length; i++) {
1859 var lineView = cm.display.view[i];
1860 if (lineView.node == lineNode)
1861 return locateNodeInLineView(lineView, node, offset);
1862 }
1863 }
1864
1865 function locateNodeInLineView(lineView, node, offset) {
1866 var wrapper = lineView.text.firstChild, bad = false;
1867 if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true);
1868 if (node == wrapper) {
1869 bad = true;
1870 node = wrapper.childNodes[offset];
1871 offset = 0;
1872 if (!node) {
1873 var line = lineView.rest ? lst(lineView.rest) : lineView.line;
1874 return badPos(Pos(lineNo(line), line.text.length), bad);
1875 }
1876 }
1877
1878 var textNode = node.nodeType == 3 ? node : null, topNode = node;
1879 if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
1880 textNode = node.firstChild;
1881 if (offset) offset = textNode.nodeValue.length;
1882 }
1883 while (topNode.parentNode != wrapper) topNode = topNode.parentNode;
1884 var measure = lineView.measure, maps = measure.maps;
1885
1886 function find(textNode, topNode, offset) {
1887 for (var i = -1; i < (maps ? maps.length : 0); i++) {
1888 var map = i < 0 ? measure.map : maps[i];
1889 for (var j = 0; j < map.length; j += 3) {
1890 var curNode = map[j + 2];
1891 if (curNode == textNode || curNode == topNode) {
1892 var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
1893 var ch = map[j] + offset;
1894 if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
1895 return Pos(line, ch);
1896 }
1897 }
1898 }
1899 }
1900 var found = find(textNode, topNode, offset);
1901 if (found) return badPos(found, bad);
1902
1903 // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems
1904 for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {
1905 found = find(after, after.firstChild, 0);
1906 if (found)
1907 return badPos(Pos(found.line, found.ch - dist), bad);
1908 else
1909 dist += after.textContent.length;
1910 }
1911 for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) {
1912 found = find(before, before.firstChild, -1);
1913 if (found)
1914 return badPos(Pos(found.line, found.ch + dist), bad);
1915 else
1916 dist += after.textContent.length;
1917 }
1918 }
1919
1920 function domTextBetween(cm, from, to, fromLine, toLine) {
1921 var text = "", closing = false;
1922 function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
1923 function walk(node) {
1924 if (node.nodeType == 1) {
1925 var cmText = node.getAttribute("cm-text");
1926 if (cmText != null) {
1927 if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
1928 text += cmText;
1929 return;
1930 }
1931 var markerID = node.getAttribute("cm-marker"), range;
1932 if (markerID) {
1933 var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID));
1934 if (found.length && (range = found[0].find()))
1935 text += getBetween(cm.doc, range.from, range.to).join("\n");
1936 return;
1937 }
1938 if (node.getAttribute("contenteditable") == "false") return;
1939 for (var i = 0; i < node.childNodes.length; i++)
1940 walk(node.childNodes[i]);
1941 if (/^(pre|div|p)$/i.test(node.nodeName))
1942 closing = true;
1943 } else if (node.nodeType == 3) {
1944 var val = node.nodeValue;
1945 if (!val) return;
1946 if (closing) {
1947 text += "\n";
1948 closing = false;
1949 }
1950 text += val;
1951 }
1952 }
1953 for (;;) {
1954 walk(from);
1955 if (from == to) break;
1956 from = from.nextSibling;
1957 }
1958 return text;
1959 }
1960
1961 CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput};
1962
1963 // SELECTION / CURSOR
1964
1965 // Selection objects are immutable. A new one is created every time
1966 // the selection changes. A selection is one or more non-overlapping
1967 // (and non-touching) ranges, sorted, and an integer that indicates
1968 // which one is the primary selection (the one that's scrolled into
1969 // view, that getCursor returns, etc).
1970 function Selection(ranges, primIndex) {
1971 this.ranges = ranges;
1972 this.primIndex = primIndex;
1973 }
1974
1975 Selection.prototype = {
1976 primary: function() { return this.ranges[this.primIndex]; },
1977 equals: function(other) {
1978 if (other == this) return true;
1979 if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false;
1980 for (var i = 0; i < this.ranges.length; i++) {
1981 var here = this.ranges[i], there = other.ranges[i];
1982 if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false;
1983 }
1984 return true;
1985 },
1986 deepCopy: function() {
1987 for (var out = [], i = 0; i < this.ranges.length; i++)
1988 out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head));
1989 return new Selection(out, this.primIndex);
1990 },
1991 somethingSelected: function() {
1992 for (var i = 0; i < this.ranges.length; i++)
1993 if (!this.ranges[i].empty()) return true;
1994 return false;
1995 },
1996 contains: function(pos, end) {
1997 if (!end) end = pos;
1998 for (var i = 0; i < this.ranges.length; i++) {
1999 var range = this.ranges[i];
2000 if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0)
2001 return i;
2002 }
2003 return -1;
2004 }
2005 };
2006
2007 function Range(anchor, head) {
2008 this.anchor = anchor; this.head = head;
2009 }
2010
2011 Range.prototype = {
2012 from: function() { return minPos(this.anchor, this.head); },
2013 to: function() { return maxPos(this.anchor, this.head); },
2014 empty: function() {
2015 return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch;
2016 }
2017 };
2018
2019 // Take an unsorted, potentially overlapping set of ranges, and
2020 // build a selection out of it. 'Consumes' ranges array (modifying
2021 // it).
2022 function normalizeSelection(ranges, primIndex) {
2023 var prim = ranges[primIndex];
2024 ranges.sort(function(a, b) { return cmp(a.from(), b.from()); });
2025 primIndex = indexOf(ranges, prim);
2026 for (var i = 1; i < ranges.length; i++) {
2027 var cur = ranges[i], prev = ranges[i - 1];
2028 if (cmp(prev.to(), cur.from()) >= 0) {
2029 var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to());
2030 var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head;
2031 if (i <= primIndex) --primIndex;
2032 ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to));
2033 }
2034 }
2035 return new Selection(ranges, primIndex);
2036 }
2037
2038 function simpleSelection(anchor, head) {
2039 return new Selection([new Range(anchor, head || anchor)], 0);
2040 }
2041
2042 // Most of the external API clips given positions to make sure they
2043 // actually exist within the document.
2044 function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
2045 function clipPos(doc, pos) {
2046 if (pos.line < doc.first) return Pos(doc.first, 0);
2047 var last = doc.first + doc.size - 1;
2048 if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
2049 return clipToLen(pos, getLine(doc, pos.line).text.length);
2050 }
2051 function clipToLen(pos, linelen) {
2052 var ch = pos.ch;
2053 if (ch == null || ch > linelen) return Pos(pos.line, linelen);
2054 else if (ch < 0) return Pos(pos.line, 0);
2055 else return pos;
2056 }
2057 function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
2058 function clipPosArray(doc, array) {
2059 for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]);
2060 return out;
2061 }
2062
2063 // SELECTION UPDATES
2064
2065 // The 'scroll' parameter given to many of these indicated whether
2066 // the new cursor position should be scrolled into view after
2067 // modifying the selection.
2068
2069 // If shift is held or the extend flag is set, extends a range to
2070 // include a given position (and optionally a second position).
2071 // Otherwise, simply returns the range between the given positions.
2072 // Used for cursor motion and such.
2073 function extendRange(doc, range, head, other) {
2074 if (doc.cm && doc.cm.display.shift || doc.extend) {
2075 var anchor = range.anchor;
2076 if (other) {
2077 var posBefore = cmp(head, anchor) < 0;
2078 if (posBefore != (cmp(other, anchor) < 0)) {
2079 anchor = head;
2080 head = other;
2081 } else if (posBefore != (cmp(head, other) < 0)) {
2082 head = other;
2083 }
2084 }
2085 return new Range(anchor, head);
2086 } else {
2087 return new Range(other || head, head);
2088 }
2089 }
2090
2091 // Extend the primary selection range, discard the rest.
2092 function extendSelection(doc, head, other, options) {
2093 setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options);
2094 }
2095
2096 // Extend all selections (pos is an array of selections with length
2097 // equal the number of selections)
2098 function extendSelections(doc, heads, options) {
2099 for (var out = [], i = 0; i < doc.sel.ranges.length; i++)
2100 out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null);
2101 var newSel = normalizeSelection(out, doc.sel.primIndex);
2102 setSelection(doc, newSel, options);
2103 }
2104
2105 // Updates a single range in the selection.
2106 function replaceOneSelection(doc, i, range, options) {
2107 var ranges = doc.sel.ranges.slice(0);
2108 ranges[i] = range;
2109 setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options);
2110 }
2111
2112 // Reset the selection to a single range.
2113 function setSimpleSelection(doc, anchor, head, options) {
2114 setSelection(doc, simpleSelection(anchor, head), options);
2115 }
2116
2117 // Give beforeSelectionChange handlers a change to influence a
2118 // selection update.
2119 function filterSelectionChange(doc, sel) {
2120 var obj = {
2121 ranges: sel.ranges,
2122 update: function(ranges) {
2123 this.ranges = [];
2124 for (var i = 0; i < ranges.length; i++)
2125 this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor),
2126 clipPos(doc, ranges[i].head));
2127 }
2128 };
2129 signal(doc, "beforeSelectionChange", doc, obj);
2130 if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
2131 if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1);
2132 else return sel;
2133 }
2134
2135 function setSelectionReplaceHistory(doc, sel, options) {
2136 var done = doc.history.done, last = lst(done);
2137 if (last && last.ranges) {
2138 done[done.length - 1] = sel;
2139 setSelectionNoUndo(doc, sel, options);
2140 } else {
2141 setSelection(doc, sel, options);
2142 }
2143 }
2144
2145 // Set a new selection.
2146 function setSelection(doc, sel, options) {
2147 setSelectionNoUndo(doc, sel, options);
2148 addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options);
2149 }
2150
2151 function setSelectionNoUndo(doc, sel, options) {
2152 if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange"))
2153 sel = filterSelectionChange(doc, sel);
2154
2155 var bias = options && options.bias ||
2156 (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1);
2157 setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true));
2158
2159 if (!(options && options.scroll === false) && doc.cm)
2160 ensureCursorVisible(doc.cm);
2161 }
2162
2163 function setSelectionInner(doc, sel) {
2164 if (sel.equals(doc.sel)) return;
2165
2166 doc.sel = sel;
2167
2168 if (doc.cm) {
2169 doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true;
2170 signalCursorActivity(doc.cm);
2171 }
2172 signalLater(doc, "cursorActivity", doc);
2173 }
2174
2175 // Verify that the selection does not partially select any atomic
2176 // marked ranges.
2177 function reCheckSelection(doc) {
2178 setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll);
2179 }
2180
2181 // Return a selection that does not partially select any atomic
2182 // ranges.
2183 function skipAtomicInSelection(doc, sel, bias, mayClear) {
2184 var out;
2185 for (var i = 0; i < sel.ranges.length; i++) {
2186 var range = sel.ranges[i];
2187 var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear);
2188 var newHead = skipAtomic(doc, range.head, bias, mayClear);
2189 if (out || newAnchor != range.anchor || newHead != range.head) {
2190 if (!out) out = sel.ranges.slice(0, i);
2191 out[i] = new Range(newAnchor, newHead);
2192 }
2193 }
2194 return out ? normalizeSelection(out, sel.primIndex) : sel;
2195 }
2196
2197 // Ensure a given position is not inside an atomic range.
2198 function skipAtomic(doc, pos, bias, mayClear) {
2199 var flipped = false, curPos = pos;
2200 var dir = bias || 1;
2201 doc.cantEdit = false;
2202 search: for (;;) {
2203 var line = getLine(doc, curPos.line);
2204 if (line.markedSpans) {
2205 for (var i = 0; i < line.markedSpans.length; ++i) {
2206 var sp = line.markedSpans[i], m = sp.marker;
2207 if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) &&
2208 (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) {
2209 if (mayClear) {
2210 signal(m, "beforeCursorEnter");
2211 if (m.explicitlyCleared) {
2212 if (!line.markedSpans) break;
2213 else {--i; continue;}
2214 }
2215 }
2216 if (!m.atomic) continue;
2217 var newPos = m.find(dir < 0 ? -1 : 1);
2218 if (cmp(newPos, curPos) == 0) {
2219 newPos.ch += dir;
2220 if (newPos.ch < 0) {
2221 if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1));
2222 else newPos = null;
2223 } else if (newPos.ch > line.text.length) {
2224 if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0);
2225 else newPos = null;
2226 }
2227 if (!newPos) {
2228 if (flipped) {
2229 // Driven in a corner -- no valid cursor position found at all
2230 // -- try again *with* clearing, if we didn't already
2231 if (!mayClear) return skipAtomic(doc, pos, bias, true);
2232 // Otherwise, turn off editing until further notice, and return the start of the doc
2233 doc.cantEdit = true;
2234 return Pos(doc.first, 0);
2235 }
2236 flipped = true; newPos = pos; dir = -dir;
2237 }
2238 }
2239 curPos = newPos;
2240 continue search;
2241 }
2242 }
2243 }
2244 return curPos;
2245 }
2246 }
2247
2248 // SELECTION DRAWING
2249
2250 function updateSelection(cm) {
2251 cm.display.input.showSelection(cm.display.input.prepareSelection());
2252 }
2253
2254 function prepareSelection(cm, primary) {
2255 var doc = cm.doc, result = {};
2256 var curFragment = result.cursors = document.createDocumentFragment();
2257 var selFragment = result.selection = document.createDocumentFragment();
2258
2259 for (var i = 0; i < doc.sel.ranges.length; i++) {
2260 if (primary === false && i == doc.sel.primIndex) continue;
2261 var range = doc.sel.ranges[i];
2262 var collapsed = range.empty();
2263 if (collapsed || cm.options.showCursorWhenSelecting)
2264 drawSelectionCursor(cm, range, curFragment);
2265 if (!collapsed)
2266 drawSelectionRange(cm, range, selFragment);
2267 }
2268 return result;
2269 }
2270
2271 // Draws a cursor for the given range
2272 function drawSelectionCursor(cm, range, output) {
2273 var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine);
2274
2275 var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor"));
2276 cursor.style.left = pos.left + "px";
2277 cursor.style.top = pos.top + "px";
2278 cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
2279
2280 if (pos.other) {
2281 // Secondary cursor, shown when on a 'jump' in bi-directional text
2282 var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"));
2283 otherCursor.style.display = "";
2284 otherCursor.style.left = pos.other.left + "px";
2285 otherCursor.style.top = pos.other.top + "px";
2286 otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
2287 }
2288 }
2289
2290 // Draws the given range as a highlighted selection
2291 function drawSelectionRange(cm, range, output) {
2292 var display = cm.display, doc = cm.doc;
2293 var fragment = document.createDocumentFragment();
2294 var padding = paddingH(cm.display), leftSide = padding.left;
2295 var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right;
2296
2297 function add(left, top, width, bottom) {
2298 if (top < 0) top = 0;
2299 top = Math.round(top);
2300 bottom = Math.round(bottom);
2301 fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
2302 "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) +
2303 "px; height: " + (bottom - top) + "px"));
2304 }
2305
2306 function drawForLine(line, fromArg, toArg) {
2307 var lineObj = getLine(doc, line);
2308 var lineLen = lineObj.text.length;
2309 var start, end;
2310 function coords(ch, bias) {
2311 return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
2312 }
2313
2314 iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
2315 var leftPos = coords(from, "left"), rightPos, left, right;
2316 if (from == to) {
2317 rightPos = leftPos;
2318 left = right = leftPos.left;
2319 } else {
2320 rightPos = coords(to - 1, "right");
2321 if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
2322 left = leftPos.left;
2323 right = rightPos.right;
2324 }
2325 if (fromArg == null && from == 0) left = leftSide;
2326 if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
2327 add(left, leftPos.top, null, leftPos.bottom);
2328 left = leftSide;
2329 if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
2330 }
2331 if (toArg == null && to == lineLen) right = rightSide;
2332 if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)
2333 start = leftPos;
2334 if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)
2335 end = rightPos;
2336 if (left < leftSide + 1) left = leftSide;
2337 add(left, rightPos.top, right - left, rightPos.bottom);
2338 });
2339 return {start: start, end: end};
2340 }
2341
2342 var sFrom = range.from(), sTo = range.to();
2343 if (sFrom.line == sTo.line) {
2344 drawForLine(sFrom.line, sFrom.ch, sTo.ch);
2345 } else {
2346 var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line);
2347 var singleVLine = visualLine(fromLine) == visualLine(toLine);
2348 var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end;
2349 var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start;
2350 if (singleVLine) {
2351 if (leftEnd.top < rightStart.top - 2) {
2352 add(leftEnd.right, leftEnd.top, null, leftEnd.bottom);
2353 add(leftSide, rightStart.top, rightStart.left, rightStart.bottom);
2354 } else {
2355 add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom);
2356 }
2357 }
2358 if (leftEnd.bottom < rightStart.top)
2359 add(leftSide, leftEnd.bottom, null, rightStart.top);
2360 }
2361
2362 output.appendChild(fragment);
2363 }
2364
2365 // Cursor-blinking
2366 function restartBlink(cm) {
2367 if (!cm.state.focused) return;
2368 var display = cm.display;
2369 clearInterval(display.blinker);
2370 var on = true;
2371 display.cursorDiv.style.visibility = "";
2372 if (cm.options.cursorBlinkRate > 0)
2373 display.blinker = setInterval(function() {
2374 display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
2375 }, cm.options.cursorBlinkRate);
2376 else if (cm.options.cursorBlinkRate < 0)
2377 display.cursorDiv.style.visibility = "hidden";
2378 }
2379
2380 // HIGHLIGHT WORKER
2381
2382 function startWorker(cm, time) {
2383 if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo)
2384 cm.state.highlight.set(time, bind(highlightWorker, cm));
2385 }
2386
2387 function highlightWorker(cm) {
2388 var doc = cm.doc;
2389 if (doc.frontier < doc.first) doc.frontier = doc.first;
2390 if (doc.frontier >= cm.display.viewTo) return;
2391 var end = +new Date + cm.options.workTime;
2392 var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
2393 var changedLines = [];
2394
2395 doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) {
2396 if (doc.frontier >= cm.display.viewFrom) { // Visible
2397 var oldStyles = line.styles;
2398 var highlighted = highlightLine(cm, line, state, true);
2399 line.styles = highlighted.styles;
2400 var oldCls = line.styleClasses, newCls = highlighted.classes;
2401 if (newCls) line.styleClasses = newCls;
2402 else if (oldCls) line.styleClasses = null;
2403 var ischange = !oldStyles || oldStyles.length != line.styles.length ||
2404 oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
2405 for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
2406 if (ischange) changedLines.push(doc.frontier);
2407 line.stateAfter = copyState(doc.mode, state);
2408 } else {
2409 processLine(cm, line.text, state);
2410 line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
2411 }
2412 ++doc.frontier;
2413 if (+new Date > end) {
2414 startWorker(cm, cm.options.workDelay);
2415 return true;
2416 }
2417 });
2418 if (changedLines.length) runInOp(cm, function() {
2419 for (var i = 0; i < changedLines.length; i++)
2420 regLineChange(cm, changedLines[i], "text");
2421 });
2422 }
2423
2424 // Finds the line to start with when starting a parse. Tries to
2425 // find a line with a stateAfter, so that it can start with a
2426 // valid state. If that fails, it returns the line with the
2427 // smallest indentation, which tends to need the least context to
2428 // parse correctly.
2429 function findStartLine(cm, n, precise) {
2430 var minindent, minline, doc = cm.doc;
2431 var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
2432 for (var search = n; search > lim; --search) {
2433 if (search <= doc.first) return doc.first;
2434 var line = getLine(doc, search - 1);
2435 if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
2436 var indented = countColumn(line.text, null, cm.options.tabSize);
2437 if (minline == null || minindent > indented) {
2438 minline = search - 1;
2439 minindent = indented;
2440 }
2441 }
2442 return minline;
2443 }
2444
2445 function getStateBefore(cm, n, precise) {
2446 var doc = cm.doc, display = cm.display;
2447 if (!doc.mode.startState) return true;
2448 var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
2449 if (!state) state = startState(doc.mode);
2450 else state = copyState(doc.mode, state);
2451 doc.iter(pos, n, function(line) {
2452 processLine(cm, line.text, state);
2453 var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo;
2454 line.stateAfter = save ? copyState(doc.mode, state) : null;
2455 ++pos;
2456 });
2457 if (precise) doc.frontier = pos;
2458 return state;
2459 }
2460
2461 // POSITION MEASUREMENT
2462
2463 function paddingTop(display) {return display.lineSpace.offsetTop;}
2464 function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
2465 function paddingH(display) {
2466 if (display.cachedPaddingH) return display.cachedPaddingH;
2467 var e = removeChildrenAndAdd(display.measure, elt("pre", "x"));
2468 var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle;
2469 var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
2470 if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data;
2471 return data;
2472 }
2473
2474 function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; }
2475 function displayWidth(cm) {
2476 return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth;
2477 }
2478 function displayHeight(cm) {
2479 return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight;
2480 }
2481
2482 // Ensure the lineView.wrapping.heights array is populated. This is
2483 // an array of bottom offsets for the lines that make up a drawn
2484 // line. When lineWrapping is on, there might be more than one
2485 // height.
2486 function ensureLineHeights(cm, lineView, rect) {
2487 var wrapping = cm.options.lineWrapping;
2488 var curWidth = wrapping && displayWidth(cm);
2489 if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) {
2490 var heights = lineView.measure.heights = [];
2491 if (wrapping) {
2492 lineView.measure.width = curWidth;
2493 var rects = lineView.text.firstChild.getClientRects();
2494 for (var i = 0; i < rects.length - 1; i++) {
2495 var cur = rects[i], next = rects[i + 1];
2496 if (Math.abs(cur.bottom - next.bottom) > 2)
2497 heights.push((cur.bottom + next.top) / 2 - rect.top);
2498 }
2499 }
2500 heights.push(rect.bottom - rect.top);
2501 }
2502 }
2503
2504 // Find a line map (mapping character offsets to text nodes) and a
2505 // measurement cache for the given line number. (A line view might
2506 // contain multiple lines when collapsed ranges are present.)
2507 function mapFromLineView(lineView, line, lineN) {
2508 if (lineView.line == line)
2509 return {map: lineView.measure.map, cache: lineView.measure.cache};
2510 for (var i = 0; i < lineView.rest.length; i++)
2511 if (lineView.rest[i] == line)
2512 return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]};
2513 for (var i = 0; i < lineView.rest.length; i++)
2514 if (lineNo(lineView.rest[i]) > lineN)
2515 return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true};
2516 }
2517
2518 // Render a line into the hidden node display.externalMeasured. Used
2519 // when measurement is needed for a line that's not in the viewport.
2520 function updateExternalMeasurement(cm, line) {
2521 line = visualLine(line);
2522 var lineN = lineNo(line);
2523 var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN);
2524 view.lineN = lineN;
2525 var built = view.built = buildLineContent(cm, view);
2526 view.text = built.pre;
2527 removeChildrenAndAdd(cm.display.lineMeasure, built.pre);
2528 return view;
2529 }
2530
2531 // Get a {top, bottom, left, right} box (in line-local coordinates)
2532 // for a given character.
2533 function measureChar(cm, line, ch, bias) {
2534 return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias);
2535 }
2536
2537 // Find a line view that corresponds to the given line number.
2538 function findViewForLine(cm, lineN) {
2539 if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo)
2540 return cm.display.view[findViewIndex(cm, lineN)];
2541 var ext = cm.display.externalMeasured;
2542 if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size)
2543 return ext;
2544 }
2545
2546 // Measurement can be split in two steps, the set-up work that
2547 // applies to the whole line, and the measurement of the actual
2548 // character. Functions like coordsChar, that need to do a lot of
2549 // measurements in a row, can thus ensure that the set-up work is
2550 // only done once.
2551 function prepareMeasureForLine(cm, line) {
2552 var lineN = lineNo(line);
2553 var view = findViewForLine(cm, lineN);
2554 if (view && !view.text)
2555 view = null;
2556 else if (view && view.changes)
2557 updateLineForChanges(cm, view, lineN, getDimensions(cm));
2558 if (!view)
2559 view = updateExternalMeasurement(cm, line);
2560
2561 var info = mapFromLineView(view, line, lineN);
2562 return {
2563 line: line, view: view, rect: null,
2564 map: info.map, cache: info.cache, before: info.before,
2565 hasHeights: false
2566 };
2567 }
2568
2569 // Given a prepared measurement object, measures the position of an
2570 // actual character (or fetches it from the cache).
2571 function measureCharPrepared(cm, prepared, ch, bias, varHeight) {
2572 if (prepared.before) ch = -1;
2573 var key = ch + (bias || ""), found;
2574 if (prepared.cache.hasOwnProperty(key)) {
2575 found = prepared.cache[key];
2576 } else {
2577 if (!prepared.rect)
2578 prepared.rect = prepared.view.text.getBoundingClientRect();
2579 if (!prepared.hasHeights) {
2580 ensureLineHeights(cm, prepared.view, prepared.rect);
2581 prepared.hasHeights = true;
2582 }
2583 found = measureCharInner(cm, prepared, ch, bias);
2584 if (!found.bogus) prepared.cache[key] = found;
2585 }
2586 return {left: found.left, right: found.right,
2587 top: varHeight ? found.rtop : found.top,
2588 bottom: varHeight ? found.rbottom : found.bottom};
2589 }
2590
2591 var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
2592
2593 function nodeAndOffsetInLineMap(map, ch, bias) {
2594 var node, start, end, collapse;
2595 // First, search the line map for the text node corresponding to,
2596 // or closest to, the target character.
2597 for (var i = 0; i < map.length; i += 3) {
2598 var mStart = map[i], mEnd = map[i + 1];
2599 if (ch < mStart) {
2600 start = 0; end = 1;
2601 collapse = "left";
2602 } else if (ch < mEnd) {
2603 start = ch - mStart;
2604 end = start + 1;
2605 } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) {
2606 end = mEnd - mStart;
2607 start = end - 1;
2608 if (ch >= mEnd) collapse = "right";
2609 }
2610 if (start != null) {
2611 node = map[i + 2];
2612 if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right"))
2613 collapse = bias;
2614 if (bias == "left" && start == 0)
2615 while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) {
2616 node = map[(i -= 3) + 2];
2617 collapse = "left";
2618 }
2619 if (bias == "right" && start == mEnd - mStart)
2620 while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) {
2621 node = map[(i += 3) + 2];
2622 collapse = "right";
2623 }
2624 break;
2625 }
2626 }
2627 return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd};
2628 }
2629
2630 function measureCharInner(cm, prepared, ch, bias) {
2631 var place = nodeAndOffsetInLineMap(prepared.map, ch, bias);
2632 var node = place.node, start = place.start, end = place.end, collapse = place.collapse;
2633
2634 var rect;
2635 if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
2636 for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned
2637 while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start;
2638 while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end;
2639 if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) {
2640 rect = node.parentNode.getBoundingClientRect();
2641 } else if (ie && cm.options.lineWrapping) {
2642 var rects = range(node, start, end).getClientRects();
2643 if (rects.length)
2644 rect = rects[bias == "right" ? rects.length - 1 : 0];
2645 else
2646 rect = nullRect;
2647 } else {
2648 rect = range(node, start, end).getBoundingClientRect() || nullRect;
2649 }
2650 if (rect.left || rect.right || start == 0) break;
2651 end = start;
2652 start = start - 1;
2653 collapse = "right";
2654 }
2655 if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
2656 } else { // If it is a widget, simply get the box for the whole widget.
2657 if (start > 0) collapse = bias = "right";
2658 var rects;
2659 if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1)
2660 rect = rects[bias == "right" ? rects.length - 1 : 0];
2661 else
2662 rect = node.getBoundingClientRect();
2663 }
2664 if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {
2665 var rSpan = node.parentNode.getClientRects()[0];
2666 if (rSpan)
2667 rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom};
2668 else
2669 rect = nullRect;
2670 }
2671
2672 var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
2673 var mid = (rtop + rbot) / 2;
2674 var heights = prepared.view.measure.heights;
2675 for (var i = 0; i < heights.length - 1; i++)
2676 if (mid < heights[i]) break;
2677 var top = i ? heights[i - 1] : 0, bot = heights[i];
2678 var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left,
2679 right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left,
2680 top: top, bottom: bot};
2681 if (!rect.left && !rect.right) result.bogus = true;
2682 if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
2683
2684 return result;
2685 }
2686
2687 // Work around problem with bounding client rects on ranges being
2688 // returned incorrectly when zoomed on IE10 and below.
2689 function maybeUpdateRectForZooming(measure, rect) {
2690 if (!window.screen || screen.logicalXDPI == null ||
2691 screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
2692 return rect;
2693 var scaleX = screen.logicalXDPI / screen.deviceXDPI;
2694 var scaleY = screen.logicalYDPI / screen.deviceYDPI;
2695 return {left: rect.left * scaleX, right: rect.right * scaleX,
2696 top: rect.top * scaleY, bottom: rect.bottom * scaleY};
2697 }
2698
2699 function clearLineMeasurementCacheFor(lineView) {
2700 if (lineView.measure) {
2701 lineView.measure.cache = {};
2702 lineView.measure.heights = null;
2703 if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
2704 lineView.measure.caches[i] = {};
2705 }
2706 }
2707
2708 function clearLineMeasurementCache(cm) {
2709 cm.display.externalMeasure = null;
2710 removeChildren(cm.display.lineMeasure);
2711 for (var i = 0; i < cm.display.view.length; i++)
2712 clearLineMeasurementCacheFor(cm.display.view[i]);
2713 }
2714
2715 function clearCaches(cm) {
2716 clearLineMeasurementCache(cm);
2717 cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null;
2718 if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
2719 cm.display.lineNumChars = null;
2720 }
2721
2722 function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
2723 function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
2724
2725 // Converts a {top, bottom, left, right} box from line-local
2726 // coordinates into another coordinate system. Context may be one of
2727 // "line", "div" (display.lineDiv), "local"/null (editor), "window",
2728 // or "page".
2729 function intoCoordSystem(cm, lineObj, rect, context) {
2730 if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
2731 var size = widgetHeight(lineObj.widgets[i]);
2732 rect.top += size; rect.bottom += size;
2733 }
2734 if (context == "line") return rect;
2735 if (!context) context = "local";
2736 var yOff = heightAtLine(lineObj);
2737 if (context == "local") yOff += paddingTop(cm.display);
2738 else yOff -= cm.display.viewOffset;
2739 if (context == "page" || context == "window") {
2740 var lOff = cm.display.lineSpace.getBoundingClientRect();
2741 yOff += lOff.top + (context == "window" ? 0 : pageScrollY());
2742 var xOff = lOff.left + (context == "window" ? 0 : pageScrollX());
2743 rect.left += xOff; rect.right += xOff;
2744 }
2745 rect.top += yOff; rect.bottom += yOff;
2746 return rect;
2747 }
2748
2749 // Coverts a box from "div" coords to another coordinate system.
2750 // Context may be "window", "page", "div", or "local"/null.
2751 function fromCoordSystem(cm, coords, context) {
2752 if (context == "div") return coords;
2753 var left = coords.left, top = coords.top;
2754 // First move into "page" coordinate system
2755 if (context == "page") {
2756 left -= pageScrollX();
2757 top -= pageScrollY();
2758 } else if (context == "local" || !context) {
2759 var localBox = cm.display.sizer.getBoundingClientRect();
2760 left += localBox.left;
2761 top += localBox.top;
2762 }
2763
2764 var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect();
2765 return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top};
2766 }
2767
2768 function charCoords(cm, pos, context, lineObj, bias) {
2769 if (!lineObj) lineObj = getLine(cm.doc, pos.line);
2770 return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context);
2771 }
2772
2773 // Returns a box for a given cursor position, which may have an
2774 // 'other' property containing the position of the secondary cursor
2775 // on a bidi boundary.
2776 function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
2777 lineObj = lineObj || getLine(cm.doc, pos.line);
2778 if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
2779 function get(ch, right) {
2780 var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
2781 if (right) m.left = m.right; else m.right = m.left;
2782 return intoCoordSystem(cm, lineObj, m, context);
2783 }
2784 function getBidi(ch, partPos) {
2785 var part = order[partPos], right = part.level % 2;
2786 if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
2787 part = order[--partPos];
2788 ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
2789 right = true;
2790 } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
2791 part = order[++partPos];
2792 ch = bidiLeft(part) - part.level % 2;
2793 right = false;
2794 }
2795 if (right && ch == part.to && ch > part.from) return get(ch - 1);
2796 return get(ch, right);
2797 }
2798 var order = getOrder(lineObj), ch = pos.ch;
2799 if (!order) return get(ch);
2800 var partPos = getBidiPartAt(order, ch);
2801 var val = getBidi(ch, partPos);
2802 if (bidiOther != null) val.other = getBidi(ch, bidiOther);
2803 return val;
2804 }
2805
2806 // Used to cheaply estimate the coordinates for a position. Used for
2807 // intermediate scroll updates.
2808 function estimateCoords(cm, pos) {
2809 var left = 0, pos = clipPos(cm.doc, pos);
2810 if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch;
2811 var lineObj = getLine(cm.doc, pos.line);
2812 var top = heightAtLine(lineObj) + paddingTop(cm.display);
2813 return {left: left, right: left, top: top, bottom: top + lineObj.height};
2814 }
2815
2816 // Positions returned by coordsChar contain some extra information.
2817 // xRel is the relative x position of the input coordinates compared
2818 // to the found position (so xRel > 0 means the coordinates are to
2819 // the right of the character position, for example). When outside
2820 // is true, that means the coordinates lie outside the line's
2821 // vertical range.
2822 function PosWithInfo(line, ch, outside, xRel) {
2823 var pos = Pos(line, ch);
2824 pos.xRel = xRel;
2825 if (outside) pos.outside = true;
2826 return pos;
2827 }
2828
2829 // Compute the character position closest to the given coordinates.
2830 // Input must be lineSpace-local ("div" coordinate system).
2831 function coordsChar(cm, x, y) {
2832 var doc = cm.doc;
2833 y += cm.display.viewOffset;
2834 if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
2835 var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
2836 if (lineN > last)
2837 return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
2838 if (x < 0) x = 0;
2839
2840 var lineObj = getLine(doc, lineN);
2841 for (;;) {
2842 var found = coordsCharInner(cm, lineObj, lineN, x, y);
2843 var merged = collapsedSpanAtEnd(lineObj);
2844 var mergedPos = merged && merged.find(0, true);
2845 if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
2846 lineN = lineNo(lineObj = mergedPos.to.line);
2847 else
2848 return found;
2849 }
2850 }
2851
2852 function coordsCharInner(cm, lineObj, lineNo, x, y) {
2853 var innerOff = y - heightAtLine(lineObj);
2854 var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
2855 var preparedMeasure = prepareMeasureForLine(cm, lineObj);
2856
2857 function getX(ch) {
2858 var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure);
2859 wrongLine = true;
2860 if (innerOff > sp.bottom) return sp.left - adjust;
2861 else if (innerOff < sp.top) return sp.left + adjust;
2862 else wrongLine = false;
2863 return sp.left;
2864 }
2865
2866 var bidi = getOrder(lineObj), dist = lineObj.text.length;
2867 var from = lineLeft(lineObj), to = lineRight(lineObj);
2868 var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
2869
2870 if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
2871 // Do a binary search between these bounds.
2872 for (;;) {
2873 if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
2874 var ch = x < fromX || x - fromX <= toX - x ? from : to;
2875 var xDiff = x - (ch == from ? fromX : toX);
2876 while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
2877 var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
2878 xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0);
2879 return pos;
2880 }
2881 var step = Math.ceil(dist / 2), middle = from + step;
2882 if (bidi) {
2883 middle = from;
2884 for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
2885 }
2886 var middleX = getX(middle);
2887 if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
2888 else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
2889 }
2890 }
2891
2892 var measureText;
2893 // Compute the default text height.
2894 function textHeight(display) {
2895 if (display.cachedTextHeight != null) return display.cachedTextHeight;
2896 if (measureText == null) {
2897 measureText = elt("pre");
2898 // Measure a bunch of lines, for browsers that compute
2899 // fractional heights.
2900 for (var i = 0; i < 49; ++i) {
2901 measureText.appendChild(document.createTextNode("x"));
2902 measureText.appendChild(elt("br"));
2903 }
2904 measureText.appendChild(document.createTextNode("x"));
2905 }
2906 removeChildrenAndAdd(display.measure, measureText);
2907 var height = measureText.offsetHeight / 50;
2908 if (height > 3) display.cachedTextHeight = height;
2909 removeChildren(display.measure);
2910 return height || 1;
2911 }
2912
2913 // Compute the default character width.
2914 function charWidth(display) {
2915 if (display.cachedCharWidth != null) return display.cachedCharWidth;
2916 var anchor = elt("span", "xxxxxxxxxx");
2917 var pre = elt("pre", [anchor]);
2918 removeChildrenAndAdd(display.measure, pre);
2919 var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10;
2920 if (width > 2) display.cachedCharWidth = width;
2921 return width || 10;
2922 }
2923
2924 // OPERATIONS
2925
2926 // Operations are used to wrap a series of changes to the editor
2927 // state in such a way that each change won't have to update the
2928 // cursor and display (which would be awkward, slow, and
2929 // error-prone). Instead, display updates are batched and then all
2930 // combined and executed at once.
2931
2932 var operationGroup = null;
2933
2934 var nextOpId = 0;
2935 // Start a new operation.
2936 function startOperation(cm) {
2937 cm.curOp = {
2938 cm: cm,
2939 viewChanged: false, // Flag that indicates that lines might need to be redrawn
2940 startHeight: cm.doc.height, // Used to detect need to update scrollbar
2941 forceUpdate: false, // Used to force a redraw
2942 updateInput: null, // Whether to reset the input textarea
2943 typing: false, // Whether this reset should be careful to leave existing text (for compositing)
2944 changeObjs: null, // Accumulated changes, for firing change events
2945 cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
2946 cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
2947 selectionChanged: false, // Whether the selection needs to be redrawn
2948 updateMaxLine: false, // Set when the widest line needs to be determined anew
2949 scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
2950 scrollToPos: null, // Used to scroll to a specific position
2951 focus: false,
2952 id: ++nextOpId // Unique ID
2953 };
2954 if (operationGroup) {
2955 operationGroup.ops.push(cm.curOp);
2956 } else {
2957 cm.curOp.ownsGroup = operationGroup = {
2958 ops: [cm.curOp],
2959 delayedCallbacks: []
2960 };
2961 }
2962 }
2963
2964 function fireCallbacksForOps(group) {
2965 // Calls delayed callbacks and cursorActivity handlers until no
2966 // new ones appear
2967 var callbacks = group.delayedCallbacks, i = 0;
2968 do {
2969 for (; i < callbacks.length; i++)
2970 callbacks[i]();
2971 for (var j = 0; j < group.ops.length; j++) {
2972 var op = group.ops[j];
2973 if (op.cursorActivityHandlers)
2974 while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
2975 op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm);
2976 }
2977 } while (i < callbacks.length);
2978 }
2979
2980 // Finish an operation, updating the display and signalling delayed events
2981 function endOperation(cm) {
2982 var op = cm.curOp, group = op.ownsGroup;
2983 if (!group) return;
2984
2985 try { fireCallbacksForOps(group); }
2986 finally {
2987 operationGroup = null;
2988 for (var i = 0; i < group.ops.length; i++)
2989 group.ops[i].cm.curOp = null;
2990 endOperations(group);
2991 }
2992 }
2993
2994 // The DOM updates done when an operation finishes are batched so
2995 // that the minimum number of relayouts are required.
2996 function endOperations(group) {
2997 var ops = group.ops;
2998 for (var i = 0; i < ops.length; i++) // Read DOM
2999 endOperation_R1(ops[i]);
3000 for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
3001 endOperation_W1(ops[i]);
3002 for (var i = 0; i < ops.length; i++) // Read DOM
3003 endOperation_R2(ops[i]);
3004 for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
3005 endOperation_W2(ops[i]);
3006 for (var i = 0; i < ops.length; i++) // Read DOM
3007 endOperation_finish(ops[i]);
3008 }
3009
3010 function endOperation_R1(op) {
3011 var cm = op.cm, display = cm.display;
3012 maybeClipScrollbars(cm);
3013 if (op.updateMaxLine) findMaxLine(cm);
3014
3015 op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
3016 op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
3017 op.scrollToPos.to.line >= display.viewTo) ||
3018 display.maxLineChanged && cm.options.lineWrapping;
3019 op.update = op.mustUpdate &&
3020 new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
3021 }
3022
3023 function endOperation_W1(op) {
3024 op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
3025 }
3026
3027 function endOperation_R2(op) {
3028 var cm = op.cm, display = cm.display;
3029 if (op.updatedDisplay) updateHeightsInViewport(cm);
3030
3031 op.barMeasure = measureForScrollbars(cm);
3032
3033 // If the max line changed since it was last measured, measure it,
3034 // and ensure the document's width matches it.
3035 // updateDisplay_W2 will use these properties to do the actual resizing
3036 if (display.maxLineChanged && !cm.options.lineWrapping) {
3037 op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
3038 cm.display.sizerWidth = op.adjustWidthTo;
3039 op.barMeasure.scrollWidth =
3040 Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth);
3041 op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm));
3042 }
3043
3044 if (op.updatedDisplay || op.selectionChanged)
3045 op.preparedSelection = display.input.prepareSelection();
3046 }
3047
3048 function endOperation_W2(op) {
3049 var cm = op.cm;
3050
3051 if (op.adjustWidthTo != null) {
3052 cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
3053 if (op.maxScrollLeft < cm.doc.scrollLeft)
3054 setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
3055 cm.display.maxLineChanged = false;
3056 }
3057
3058 if (op.preparedSelection)
3059 cm.display.input.showSelection(op.preparedSelection);
3060 if (op.updatedDisplay)
3061 setDocumentHeight(cm, op.barMeasure);
3062 if (op.updatedDisplay || op.startHeight != cm.doc.height)
3063 updateScrollbars(cm, op.barMeasure);
3064
3065 if (op.selectionChanged) restartBlink(cm);
3066
3067 if (cm.state.focused && op.updateInput)
3068 cm.display.input.reset(op.typing);
3069 if (op.focus && op.focus == activeElt()) ensureFocus(op.cm);
3070 }
3071
3072 function endOperation_finish(op) {
3073 var cm = op.cm, display = cm.display, doc = cm.doc;
3074
3075 if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
3076
3077 // Abort mouse wheel delta measurement, when scrolling explicitly
3078 if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
3079 display.wheelStartX = display.wheelStartY = null;
3080
3081 // Propagate the scroll position to the actual DOM scroller
3082 if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {
3083 doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop));
3084 display.scrollbars.setScrollTop(doc.scrollTop);
3085 display.scroller.scrollTop = doc.scrollTop;
3086 }
3087 if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {
3088 doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft));
3089 display.scrollbars.setScrollLeft(doc.scrollLeft);
3090 display.scroller.scrollLeft = doc.scrollLeft;
3091 alignHorizontally(cm);
3092 }
3093 // If we need to scroll a specific position into view, do so.
3094 if (op.scrollToPos) {
3095 var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
3096 clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
3097 if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
3098 }
3099
3100 // Fire events for markers that are hidden/unidden by editing or
3101 // undoing
3102 var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
3103 if (hidden) for (var i = 0; i < hidden.length; ++i)
3104 if (!hidden[i].lines.length) signal(hidden[i], "hide");
3105 if (unhidden) for (var i = 0; i < unhidden.length; ++i)
3106 if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
3107
3108 if (display.wrapper.offsetHeight)
3109 doc.scrollTop = cm.display.scroller.scrollTop;
3110
3111 // Fire change events, and delayed event handlers
3112 if (op.changeObjs)
3113 signal(cm, "changes", cm, op.changeObjs);
3114 if (op.update)
3115 op.update.finish();
3116 }
3117
3118 // Run the given function in an operation
3119 function runInOp(cm, f) {
3120 if (cm.curOp) return f();
3121 startOperation(cm);
3122 try { return f(); }
3123 finally { endOperation(cm); }
3124 }
3125 // Wraps a function in an operation. Returns the wrapped function.
3126 function operation(cm, f) {
3127 return function() {
3128 if (cm.curOp) return f.apply(cm, arguments);
3129 startOperation(cm);
3130 try { return f.apply(cm, arguments); }
3131 finally { endOperation(cm); }
3132 };
3133 }
3134 // Used to add methods to editor and doc instances, wrapping them in
3135 // operations.
3136 function methodOp(f) {
3137 return function() {
3138 if (this.curOp) return f.apply(this, arguments);
3139 startOperation(this);
3140 try { return f.apply(this, arguments); }
3141 finally { endOperation(this); }
3142 };
3143 }
3144 function docMethodOp(f) {
3145 return function() {
3146 var cm = this.cm;
3147 if (!cm || cm.curOp) return f.apply(this, arguments);
3148 startOperation(cm);
3149 try { return f.apply(this, arguments); }
3150 finally { endOperation(cm); }
3151 };
3152 }
3153
3154 // VIEW TRACKING
3155
3156 // These objects are used to represent the visible (currently drawn)
3157 // part of the document. A LineView may correspond to multiple
3158 // logical lines, if those are connected by collapsed ranges.
3159 function LineView(doc, line, lineN) {
3160 // The starting line
3161 this.line = line;
3162 // Continuing lines, if any
3163 this.rest = visualLineContinued(line);
3164 // Number of logical lines in this visual line
3165 this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1;
3166 this.node = this.text = null;
3167 this.hidden = lineIsHidden(doc, line);
3168 }
3169
3170 // Create a range of LineView objects for the given lines.
3171 function buildViewArray(cm, from, to) {
3172 var array = [], nextPos;
3173 for (var pos = from; pos < to; pos = nextPos) {
3174 var view = new LineView(cm.doc, getLine(cm.doc, pos), pos);
3175 nextPos = pos + view.size;
3176 array.push(view);
3177 }
3178 return array;
3179 }
3180
3181 // Updates the display.view data structure for a given change to the
3182 // document. From and to are in pre-change coordinates. Lendiff is
3183 // the amount of lines added or subtracted by the change. This is
3184 // used for changes that span multiple lines, or change the way
3185 // lines are divided into visual lines. regLineChange (below)
3186 // registers single-line changes.
3187 function regChange(cm, from, to, lendiff) {
3188 if (from == null) from = cm.doc.first;
3189 if (to == null) to = cm.doc.first + cm.doc.size;
3190 if (!lendiff) lendiff = 0;
3191
3192 var display = cm.display;
3193 if (lendiff && to < display.viewTo &&
3194 (display.updateLineNumbers == null || display.updateLineNumbers > from))
3195 display.updateLineNumbers = from;
3196
3197 cm.curOp.viewChanged = true;
3198
3199 if (from >= display.viewTo) { // Change after
3200 if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo)
3201 resetView(cm);
3202 } else if (to <= display.viewFrom) { // Change before
3203 if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) {
3204 resetView(cm);
3205 } else {
3206 display.viewFrom += lendiff;
3207 display.viewTo += lendiff;
3208 }
3209 } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap
3210 resetView(cm);
3211 } else if (from <= display.viewFrom) { // Top overlap
3212 var cut = viewCuttingPoint(cm, to, to + lendiff, 1);
3213 if (cut) {
3214 display.view = display.view.slice(cut.index);
3215 display.viewFrom = cut.lineN;
3216 display.viewTo += lendiff;
3217 } else {
3218 resetView(cm);
3219 }
3220 } else if (to >= display.viewTo) { // Bottom overlap
3221 var cut = viewCuttingPoint(cm, from, from, -1);
3222 if (cut) {
3223 display.view = display.view.slice(0, cut.index);
3224 display.viewTo = cut.lineN;
3225 } else {
3226 resetView(cm);
3227 }
3228 } else { // Gap in the middle
3229 var cutTop = viewCuttingPoint(cm, from, from, -1);
3230 var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1);
3231 if (cutTop && cutBot) {
3232 display.view = display.view.slice(0, cutTop.index)
3233 .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN))
3234 .concat(display.view.slice(cutBot.index));
3235 display.viewTo += lendiff;
3236 } else {
3237 resetView(cm);
3238 }
3239 }
3240
3241 var ext = display.externalMeasured;
3242 if (ext) {
3243 if (to < ext.lineN)
3244 ext.lineN += lendiff;
3245 else if (from < ext.lineN + ext.size)
3246 display.externalMeasured = null;
3247 }
3248 }
3249
3250 // Register a change to a single line. Type must be one of "text",
3251 // "gutter", "class", "widget"
3252 function regLineChange(cm, line, type) {
3253 cm.curOp.viewChanged = true;
3254 var display = cm.display, ext = cm.display.externalMeasured;
3255 if (ext && line >= ext.lineN && line < ext.lineN + ext.size)
3256 display.externalMeasured = null;
3257
3258 if (line < display.viewFrom || line >= display.viewTo) return;
3259 var lineView = display.view[findViewIndex(cm, line)];
3260 if (lineView.node == null) return;
3261 var arr = lineView.changes || (lineView.changes = []);
3262 if (indexOf(arr, type) == -1) arr.push(type);
3263 }
3264
3265 // Clear the view.
3266 function resetView(cm) {
3267 cm.display.viewFrom = cm.display.viewTo = cm.doc.first;
3268 cm.display.view = [];
3269 cm.display.viewOffset = 0;
3270 }
3271
3272 // Find the view element corresponding to a given line. Return null
3273 // when the line isn't visible.
3274 function findViewIndex(cm, n) {
3275 if (n >= cm.display.viewTo) return null;
3276 n -= cm.display.viewFrom;
3277 if (n < 0) return null;
3278 var view = cm.display.view;
3279 for (var i = 0; i < view.length; i++) {
3280 n -= view[i].size;
3281 if (n < 0) return i;
3282 }
3283 }
3284
3285 function viewCuttingPoint(cm, oldN, newN, dir) {
3286 var index = findViewIndex(cm, oldN), diff, view = cm.display.view;
3287 if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size)
3288 return {index: index, lineN: newN};
3289 for (var i = 0, n = cm.display.viewFrom; i < index; i++)
3290 n += view[i].size;
3291 if (n != oldN) {
3292 if (dir > 0) {
3293 if (index == view.length - 1) return null;
3294 diff = (n + view[index].size) - oldN;
3295 index++;
3296 } else {
3297 diff = n - oldN;
3298 }
3299 oldN += diff; newN += diff;
3300 }
3301 while (visualLineNo(cm.doc, newN) != newN) {
3302 if (index == (dir < 0 ? 0 : view.length - 1)) return null;
3303 newN += dir * view[index - (dir < 0 ? 1 : 0)].size;
3304 index += dir;
3305 }
3306 return {index: index, lineN: newN};
3307 }
3308
3309 // Force the view to cover a given range, adding empty view element
3310 // or clipping off existing ones as needed.
3311 function adjustView(cm, from, to) {
3312 var display = cm.display, view = display.view;
3313 if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) {
3314 display.view = buildViewArray(cm, from, to);
3315 display.viewFrom = from;
3316 } else {
3317 if (display.viewFrom > from)
3318 display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view);
3319 else if (display.viewFrom < from)
3320 display.view = display.view.slice(findViewIndex(cm, from));
3321 display.viewFrom = from;
3322 if (display.viewTo < to)
3323 display.view = display.view.concat(buildViewArray(cm, display.viewTo, to));
3324 else if (display.viewTo > to)
3325 display.view = display.view.slice(0, findViewIndex(cm, to));
3326 }
3327 display.viewTo = to;
3328 }
3329
3330 // Count the number of lines in the view whose DOM representation is
3331 // out of date (or nonexistent).
3332 function countDirtyView(cm) {
3333 var view = cm.display.view, dirty = 0;
3334 for (var i = 0; i < view.length; i++) {
3335 var lineView = view[i];
3336 if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty;
3337 }
3338 return dirty;
3339 }
3340
3341 // EVENT HANDLERS
3342
3343 // Attach the necessary event handlers when initializing the editor
3344 function registerEventHandlers(cm) {
3345 var d = cm.display;
3346 on(d.scroller, "mousedown", operation(cm, onMouseDown));
3347 // Older IE's will not fire a second mousedown for a double click
3348 if (ie && ie_version < 11)
3349 on(d.scroller, "dblclick", operation(cm, function(e) {
3350 if (signalDOMEvent(cm, e)) return;
3351 var pos = posFromMouse(cm, e);
3352 if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
3353 e_preventDefault(e);
3354 var word = cm.findWordAt(pos);
3355 extendSelection(cm.doc, word.anchor, word.head);
3356 }));
3357 else
3358 on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
3359 // Some browsers fire contextmenu *after* opening the menu, at
3360 // which point we can't mess with it anymore. Context menu is
3361 // handled in onMouseDown for these browsers.
3362 if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
3363
3364 // Used to suppress mouse event handling when a touch happens
3365 var touchFinished, prevTouch = {end: 0};
3366 function finishTouch() {
3367 if (d.activeTouch) {
3368 touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000);
3369 prevTouch = d.activeTouch;
3370 prevTouch.end = +new Date;
3371 }
3372 };
3373 function isMouseLikeTouchEvent(e) {
3374 if (e.touches.length != 1) return false;
3375 var touch = e.touches[0];
3376 return touch.radiusX <= 1 && touch.radiusY <= 1;
3377 }
3378 function farAway(touch, other) {
3379 if (other.left == null) return true;
3380 var dx = other.left - touch.left, dy = other.top - touch.top;
3381 return dx * dx + dy * dy > 20 * 20;
3382 }
3383 on(d.scroller, "touchstart", function(e) {
3384 if (!isMouseLikeTouchEvent(e)) {
3385 clearTimeout(touchFinished);
3386 var now = +new Date;
3387 d.activeTouch = {start: now, moved: false,
3388 prev: now - prevTouch.end <= 300 ? prevTouch : null};
3389 if (e.touches.length == 1) {
3390 d.activeTouch.left = e.touches[0].pageX;
3391 d.activeTouch.top = e.touches[0].pageY;
3392 }
3393 }
3394 });
3395 on(d.scroller, "touchmove", function() {
3396 if (d.activeTouch) d.activeTouch.moved = true;
3397 });
3398 on(d.scroller, "touchend", function(e) {
3399 var touch = d.activeTouch;
3400 if (touch && !eventInWidget(d, e) && touch.left != null &&
3401 !touch.moved && new Date - touch.start < 300) {
3402 var pos = cm.coordsChar(d.activeTouch, "page"), range;
3403 if (!touch.prev || farAway(touch, touch.prev)) // Single tap
3404 range = new Range(pos, pos);
3405 else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap
3406 range = cm.findWordAt(pos);
3407 else // Triple tap
3408 range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)));
3409 cm.setSelection(range.anchor, range.head);
3410 cm.focus();
3411 e_preventDefault(e);
3412 }
3413 finishTouch();
3414 });
3415 on(d.scroller, "touchcancel", finishTouch);
3416
3417 // Sync scrolling between fake scrollbars and real scrollable
3418 // area, ensure viewport is updated when scrolling.
3419 on(d.scroller, "scroll", function() {
3420 if (d.scroller.clientHeight) {
3421 setScrollTop(cm, d.scroller.scrollTop);
3422 setScrollLeft(cm, d.scroller.scrollLeft, true);
3423 signal(cm, "scroll", cm);
3424 }
3425 });
3426
3427 // Listen to wheel events in order to try and update the viewport on time.
3428 on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
3429 on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
3430
3431 // Prevent wrapper from ever scrolling
3432 on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
3433
3434 d.dragFunctions = {
3435 simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);},
3436 start: function(e){onDragStart(cm, e);},
3437 drop: operation(cm, onDrop)
3438 };
3439
3440 var inp = d.input.getField();
3441 on(inp, "keyup", function(e) { onKeyUp.call(cm, e); });
3442 on(inp, "keydown", operation(cm, onKeyDown));
3443 on(inp, "keypress", operation(cm, onKeyPress));
3444 on(inp, "focus", bind(onFocus, cm));
3445 on(inp, "blur", bind(onBlur, cm));
3446 }
3447
3448 function dragDropChanged(cm, value, old) {
3449 var wasOn = old && old != CodeMirror.Init;
3450 if (!value != !wasOn) {
3451 var funcs = cm.display.dragFunctions;
3452 var toggle = value ? on : off;
3453 toggle(cm.display.scroller, "dragstart", funcs.start);
3454 toggle(cm.display.scroller, "dragenter", funcs.simple);
3455 toggle(cm.display.scroller, "dragover", funcs.simple);
3456 toggle(cm.display.scroller, "drop", funcs.drop);
3457 }
3458 }
3459
3460 // Called when the window resizes
3461 function onResize(cm) {
3462 var d = cm.display;
3463 if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
3464 return;
3465 // Might be a text scaling operation, clear size caches.
3466 d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
3467 d.scrollbarsClipped = false;
3468 cm.setSize();
3469 }
3470
3471 // MOUSE EVENTS
3472
3473 // Return true when the given mouse event happened in a widget
3474 function eventInWidget(display, e) {
3475 for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
3476 if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") ||
3477 (n.parentNode == display.sizer && n != display.mover))
3478 return true;
3479 }
3480 }
3481
3482 // Given a mouse event, find the corresponding position. If liberal
3483 // is false, it checks whether a gutter or scrollbar was clicked,
3484 // and returns null if it was. forRect is used by rectangular
3485 // selections, and tries to estimate a character position even for
3486 // coordinates beyond the right of the text.
3487 function posFromMouse(cm, e, liberal, forRect) {
3488 var display = cm.display;
3489 if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null;
3490
3491 var x, y, space = display.lineSpace.getBoundingClientRect();
3492 // Fails unpredictably on IE[67] when mouse is dragged around quickly.
3493 try { x = e.clientX - space.left; y = e.clientY - space.top; }
3494 catch (e) { return null; }
3495 var coords = coordsChar(cm, x, y), line;
3496 if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) {
3497 var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length;
3498 coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff));
3499 }
3500 return coords;
3501 }
3502
3503 // A mouse down can be a single click, double click, triple click,
3504 // start of selection drag, start of text drag, new cursor
3505 // (ctrl-click), rectangle drag (alt-drag), or xwin
3506 // middle-click-paste. Or it might be a click on something we should
3507 // not interfere with, such as a scrollbar or widget.
3508 function onMouseDown(e) {
3509 var cm = this, display = cm.display;
3510 if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return;
3511 display.shift = e.shiftKey;
3512
3513 if (eventInWidget(display, e)) {
3514 if (!webkit) {
3515 // Briefly turn off draggability, to allow widgets to do
3516 // normal dragging things.
3517 display.scroller.draggable = false;
3518 setTimeout(function(){display.scroller.draggable = true;}, 100);
3519 }
3520 return;
3521 }
3522 if (clickInGutter(cm, e)) return;
3523 var start = posFromMouse(cm, e);
3524 window.focus();
3525
3526 switch (e_button(e)) {
3527 case 1:
3528 if (start)
3529 leftButtonDown(cm, e, start);
3530 else if (e_target(e) == display.scroller)
3531 e_preventDefault(e);
3532 break;
3533 case 2:
3534 if (webkit) cm.state.lastMiddleDown = +new Date;
3535 if (start) extendSelection(cm.doc, start);
3536 setTimeout(function() {display.input.focus();}, 20);
3537 e_preventDefault(e);
3538 break;
3539 case 3:
3540 if (captureRightClick) onContextMenu(cm, e);
3541 else delayBlurEvent(cm);
3542 break;
3543 }
3544 }
3545
3546 var lastClick, lastDoubleClick;
3547 function leftButtonDown(cm, e, start) {
3548 if (ie) setTimeout(bind(ensureFocus, cm), 0);
3549 else cm.curOp.focus = activeElt();
3550
3551 var now = +new Date, type;
3552 if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {
3553 type = "triple";
3554 } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {
3555 type = "double";
3556 lastDoubleClick = {time: now, pos: start};
3557 } else {
3558 type = "single";
3559 lastClick = {time: now, pos: start};
3560 }
3561
3562 var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained;
3563 if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) &&
3564 type == "single" && (contained = sel.contains(start)) > -1 &&
3565 !sel.ranges[contained].empty())
3566 leftButtonStartDrag(cm, e, start, modifier);
3567 else
3568 leftButtonSelect(cm, e, start, type, modifier);
3569 }
3570
3571 // Start a text drag. When it ends, see if any dragging actually
3572 // happen, and treat as a click if it didn't.
3573 function leftButtonStartDrag(cm, e, start, modifier) {
3574 var display = cm.display, startTime = +new Date;
3575 var dragEnd = operation(cm, function(e2) {
3576 if (webkit) display.scroller.draggable = false;
3577 cm.state.draggingText = false;
3578 off(document, "mouseup", dragEnd);
3579 off(display.scroller, "drop", dragEnd);
3580 if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
3581 e_preventDefault(e2);
3582 if (!modifier && +new Date - 200 < startTime)
3583 extendSelection(cm.doc, start);
3584 // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
3585 if (webkit || ie && ie_version == 9)
3586 setTimeout(function() {document.body.focus(); display.input.focus();}, 20);
3587 else
3588 display.input.focus();
3589 }
3590 });
3591 // Let the drag handler handle this.
3592 if (webkit) display.scroller.draggable = true;
3593 cm.state.draggingText = dragEnd;
3594 // IE's approach to draggable
3595 if (display.scroller.dragDrop) display.scroller.dragDrop();
3596 on(document, "mouseup", dragEnd);
3597 on(display.scroller, "drop", dragEnd);
3598 }
3599
3600 // Normal selection, as opposed to text dragging.
3601 function leftButtonSelect(cm, e, start, type, addNew) {
3602 var display = cm.display, doc = cm.doc;
3603 e_preventDefault(e);
3604
3605 var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges;
3606 if (addNew && !e.shiftKey) {
3607 ourIndex = doc.sel.contains(start);
3608 if (ourIndex > -1)
3609 ourRange = ranges[ourIndex];
3610 else
3611 ourRange = new Range(start, start);
3612 } else {
3613 ourRange = doc.sel.primary();
3614 ourIndex = doc.sel.primIndex;
3615 }
3616
3617 if (e.altKey) {
3618 type = "rect";
3619 if (!addNew) ourRange = new Range(start, start);
3620 start = posFromMouse(cm, e, true, true);
3621 ourIndex = -1;
3622 } else if (type == "double") {
3623 var word = cm.findWordAt(start);
3624 if (cm.display.shift || doc.extend)
3625 ourRange = extendRange(doc, ourRange, word.anchor, word.head);
3626 else
3627 ourRange = word;
3628 } else if (type == "triple") {
3629 var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0)));
3630 if (cm.display.shift || doc.extend)
3631 ourRange = extendRange(doc, ourRange, line.anchor, line.head);
3632 else
3633 ourRange = line;
3634 } else {
3635 ourRange = extendRange(doc, ourRange, start);
3636 }
3637
3638 if (!addNew) {
3639 ourIndex = 0;
3640 setSelection(doc, new Selection([ourRange], 0), sel_mouse);
3641 startSel = doc.sel;
3642 } else if (ourIndex == -1) {
3643 ourIndex = ranges.length;
3644 setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
3645 {scroll: false, origin: "*mouse"});
3646 } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) {
3647 setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0));
3648 startSel = doc.sel;
3649 } else {
3650 replaceOneSelection(doc, ourIndex, ourRange, sel_mouse);
3651 }
3652
3653 var lastPos = start;
3654 function extendTo(pos) {
3655 if (cmp(lastPos, pos) == 0) return;
3656 lastPos = pos;
3657
3658 if (type == "rect") {
3659 var ranges = [], tabSize = cm.options.tabSize;
3660 var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize);
3661 var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize);
3662 var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol);
3663 for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line));
3664 line <= end; line++) {
3665 var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize);
3666 if (left == right)
3667 ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos)));
3668 else if (text.length > leftPos)
3669 ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize))));
3670 }
3671 if (!ranges.length) ranges.push(new Range(start, start));
3672 setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),
3673 {origin: "*mouse", scroll: false});
3674 cm.scrollIntoView(pos);
3675 } else {
3676 var oldRange = ourRange;
3677 var anchor = oldRange.anchor, head = pos;
3678 if (type != "single") {
3679 if (type == "double")
3680 var range = cm.findWordAt(pos);
3681 else
3682 var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0)));
3683 if (cmp(range.anchor, anchor) > 0) {
3684 head = range.head;
3685 anchor = minPos(oldRange.from(), range.anchor);
3686 } else {
3687 head = range.anchor;
3688 anchor = maxPos(oldRange.to(), range.head);
3689 }
3690 }
3691 var ranges = startSel.ranges.slice(0);
3692 ranges[ourIndex] = new Range(clipPos(doc, anchor), head);
3693 setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse);
3694 }
3695 }
3696
3697 var editorSize = display.wrapper.getBoundingClientRect();
3698 // Used to ensure timeout re-tries don't fire when another extend
3699 // happened in the meantime (clearTimeout isn't reliable -- at
3700 // least on Chrome, the timeouts still happen even when cleared,
3701 // if the clear happens after their scheduled firing time).
3702 var counter = 0;
3703
3704 function extend(e) {
3705 var curCount = ++counter;
3706 var cur = posFromMouse(cm, e, true, type == "rect");
3707 if (!cur) return;
3708 if (cmp(cur, lastPos) != 0) {
3709 cm.curOp.focus = activeElt();
3710 extendTo(cur);
3711 var visible = visibleLines(display, doc);
3712 if (cur.line >= visible.to || cur.line < visible.from)
3713 setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
3714 } else {
3715 var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
3716 if (outside) setTimeout(operation(cm, function() {
3717 if (counter != curCount) return;
3718 display.scroller.scrollTop += outside;
3719 extend(e);
3720 }), 50);
3721 }
3722 }
3723
3724 function done(e) {
3725 counter = Infinity;
3726 e_preventDefault(e);
3727 display.input.focus();
3728 off(document, "mousemove", move);
3729 off(document, "mouseup", up);
3730 doc.history.lastSelOrigin = null;
3731 }
3732
3733 var move = operation(cm, function(e) {
3734 if (!e_button(e)) done(e);
3735 else extend(e);
3736 });
3737 var up = operation(cm, done);
3738 on(document, "mousemove", move);
3739 on(document, "mouseup", up);
3740 }
3741
3742 // Determines whether an event happened in the gutter, and fires the
3743 // handlers for the corresponding event.
3744 function gutterEvent(cm, e, type, prevent, signalfn) {
3745 try { var mX = e.clientX, mY = e.clientY; }
3746 catch(e) { return false; }
3747 if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false;
3748 if (prevent) e_preventDefault(e);
3749
3750 var display = cm.display;
3751 var lineBox = display.lineDiv.getBoundingClientRect();
3752
3753 if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e);
3754 mY -= lineBox.top - display.viewOffset;
3755
3756 for (var i = 0; i < cm.options.gutters.length; ++i) {
3757 var g = display.gutters.childNodes[i];
3758 if (g && g.getBoundingClientRect().right >= mX) {
3759 var line = lineAtHeight(cm.doc, mY);
3760 var gutter = cm.options.gutters[i];
3761 signalfn(cm, type, cm, line, gutter, e);
3762 return e_defaultPrevented(e);
3763 }
3764 }
3765 }
3766
3767 function clickInGutter(cm, e) {
3768 return gutterEvent(cm, e, "gutterClick", true, signalLater);
3769 }
3770
3771 // Kludge to work around strange IE behavior where it'll sometimes
3772 // re-fire a series of drag-related events right after the drop (#1551)
3773 var lastDrop = 0;
3774
3775 function onDrop(e) {
3776 var cm = this;
3777 if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))
3778 return;
3779 e_preventDefault(e);
3780 if (ie) lastDrop = +new Date;
3781 var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
3782 if (!pos || isReadOnly(cm)) return;
3783 // Might be a file drop, in which case we simply extract the text
3784 // and insert it.
3785 if (files && files.length && window.FileReader && window.File) {
3786 var n = files.length, text = Array(n), read = 0;
3787 var loadFile = function(file, i) {
3788 var reader = new FileReader;
3789 reader.onload = operation(cm, function() {
3790 text[i] = reader.result;
3791 if (++read == n) {
3792 pos = clipPos(cm.doc, pos);
3793 var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"};
3794 makeChange(cm.doc, change);
3795 setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change)));
3796 }
3797 });
3798 reader.readAsText(file);
3799 };
3800 for (var i = 0; i < n; ++i) loadFile(files[i], i);
3801 } else { // Normal drop
3802 // Don't do a replace if the drop happened inside of the selected text.
3803 if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {
3804 cm.state.draggingText(e);
3805 // Ensure the editor is re-focused
3806 setTimeout(function() {cm.display.input.focus();}, 20);
3807 return;
3808 }
3809 try {
3810 var text = e.dataTransfer.getData("Text");
3811 if (text) {
3812 if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey))
3813 var selected = cm.listSelections();
3814 setSelectionNoUndo(cm.doc, simpleSelection(pos, pos));
3815 if (selected) for (var i = 0; i < selected.length; ++i)
3816 replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag");
3817 cm.replaceSelection(text, "around", "paste");
3818 cm.display.input.focus();
3819 }
3820 }
3821 catch(e){}
3822 }
3823 }
3824
3825 function onDragStart(cm, e) {
3826 if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
3827 if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
3828
3829 e.dataTransfer.setData("Text", cm.getSelection());
3830
3831 // Use dummy image instead of default browsers image.
3832 // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
3833 if (e.dataTransfer.setDragImage && !safari) {
3834 var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
3835 img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
3836 if (presto) {
3837 img.width = img.height = 1;
3838 cm.display.wrapper.appendChild(img);
3839 // Force a relayout, or Opera won't use our image for some obscure reason
3840 img._top = img.offsetTop;
3841 }
3842 e.dataTransfer.setDragImage(img, 0, 0);
3843 if (presto) img.parentNode.removeChild(img);
3844 }
3845 }
3846
3847 // SCROLL EVENTS
3848
3849 // Sync the scrollable area and scrollbars, ensure the viewport
3850 // covers the visible area.
3851 function setScrollTop(cm, val) {
3852 if (Math.abs(cm.doc.scrollTop - val) < 2) return;
3853 cm.doc.scrollTop = val;
3854 if (!gecko) updateDisplaySimple(cm, {top: val});
3855 if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
3856 cm.display.scrollbars.setScrollTop(val);
3857 if (gecko) updateDisplaySimple(cm);
3858 startWorker(cm, 100);
3859 }
3860 // Sync scroller and scrollbar, ensure the gutter elements are
3861 // aligned.
3862 function setScrollLeft(cm, val, isScroller) {
3863 if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
3864 val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
3865 cm.doc.scrollLeft = val;
3866 alignHorizontally(cm);
3867 if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
3868 cm.display.scrollbars.setScrollLeft(val);
3869 }
3870
3871 // Since the delta values reported on mouse wheel events are
3872 // unstandardized between browsers and even browser versions, and
3873 // generally horribly unpredictable, this code starts by measuring
3874 // the scroll effect that the first few mouse wheel events have,
3875 // and, from that, detects the way it can convert deltas to pixel
3876 // offsets afterwards.
3877 //
3878 // The reason we want to know the amount a wheel event will scroll
3879 // is that it gives us a chance to update the display before the
3880 // actual scrolling happens, reducing flickering.
3881
3882 var wheelSamples = 0, wheelPixelsPerUnit = null;
3883 // Fill in a browser-detected starting value on browsers where we
3884 // know one. These don't have to be accurate -- the result of them
3885 // being wrong would just be a slight flicker on the first wheel
3886 // scroll (if it is large enough).
3887 if (ie) wheelPixelsPerUnit = -.53;
3888 else if (gecko) wheelPixelsPerUnit = 15;
3889 else if (chrome) wheelPixelsPerUnit = -.7;
3890 else if (safari) wheelPixelsPerUnit = -1/3;
3891
3892 var wheelEventDelta = function(e) {
3893 var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
3894 if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
3895 if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
3896 else if (dy == null) dy = e.wheelDelta;
3897 return {x: dx, y: dy};
3898 };
3899 CodeMirror.wheelEventPixels = function(e) {
3900 var delta = wheelEventDelta(e);
3901 delta.x *= wheelPixelsPerUnit;
3902 delta.y *= wheelPixelsPerUnit;
3903 return delta;
3904 };
3905
3906 function onScrollWheel(cm, e) {
3907 var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
3908
3909 var display = cm.display, scroll = display.scroller;
3910 // Quit if there's nothing to scroll here
3911 if (!(dx && scroll.scrollWidth > scroll.clientWidth ||
3912 dy && scroll.scrollHeight > scroll.clientHeight)) return;
3913
3914 // Webkit browsers on OS X abort momentum scrolls when the target
3915 // of the scroll event is removed from the scrollable element.
3916 // This hack (see related code in patchDisplay) makes sure the
3917 // element is kept around.
3918 if (dy && mac && webkit) {
3919 outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) {
3920 for (var i = 0; i < view.length; i++) {
3921 if (view[i].node == cur) {
3922 cm.display.currentWheelTarget = cur;
3923 break outer;
3924 }
3925 }
3926 }
3927 }
3928
3929 // On some browsers, horizontal scrolling will cause redraws to
3930 // happen before the gutter has been realigned, causing it to
3931 // wriggle around in a most unseemly way. When we have an
3932 // estimated pixels/delta value, we just handle horizontal
3933 // scrolling entirely here. It'll be slightly off from native, but
3934 // better than glitching out.
3935 if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {
3936 if (dy)
3937 setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
3938 setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
3939 e_preventDefault(e);
3940 display.wheelStartX = null; // Abort measurement, if in progress
3941 return;
3942 }
3943
3944 // 'Project' the visible viewport to cover the area that is being
3945 // scrolled into view (if we know enough to estimate it).
3946 if (dy && wheelPixelsPerUnit != null) {
3947 var pixels = dy * wheelPixelsPerUnit;
3948 var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
3949 if (pixels < 0) top = Math.max(0, top + pixels - 50);
3950 else bot = Math.min(cm.doc.height, bot + pixels + 50);
3951 updateDisplaySimple(cm, {top: top, bottom: bot});
3952 }
3953
3954 if (wheelSamples < 20) {
3955 if (display.wheelStartX == null) {
3956 display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
3957 display.wheelDX = dx; display.wheelDY = dy;
3958 setTimeout(function() {
3959 if (display.wheelStartX == null) return;
3960 var movedX = scroll.scrollLeft - display.wheelStartX;
3961 var movedY = scroll.scrollTop - display.wheelStartY;
3962 var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
3963 (movedX && display.wheelDX && movedX / display.wheelDX);
3964 display.wheelStartX = display.wheelStartY = null;
3965 if (!sample) return;
3966 wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
3967 ++wheelSamples;
3968 }, 200);
3969 } else {
3970 display.wheelDX += dx; display.wheelDY += dy;
3971 }
3972 }
3973 }
3974
3975 // KEY EVENTS
3976
3977 // Run a handler that was bound to a key.
3978 function doHandleBinding(cm, bound, dropShift) {
3979 if (typeof bound == "string") {
3980 bound = commands[bound];
3981 if (!bound) return false;
3982 }
3983 // Ensure previous input has been read, so that the handler sees a
3984 // consistent view of the document
3985 cm.display.input.ensurePolled();
3986 var prevShift = cm.display.shift, done = false;
3987 try {
3988 if (isReadOnly(cm)) cm.state.suppressEdits = true;
3989 if (dropShift) cm.display.shift = false;
3990 done = bound(cm) != Pass;
3991 } finally {
3992 cm.display.shift = prevShift;
3993 cm.state.suppressEdits = false;
3994 }
3995 return done;
3996 }
3997
3998 function lookupKeyForEditor(cm, name, handle) {
3999 for (var i = 0; i < cm.state.keyMaps.length; i++) {
4000 var result = lookupKey(name, cm.state.keyMaps[i], handle, cm);
4001 if (result) return result;
4002 }
4003 return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))
4004 || lookupKey(name, cm.options.keyMap, handle, cm);
4005 }
4006
4007 var stopSeq = new Delayed;
4008 function dispatchKey(cm, name, e, handle) {
4009 var seq = cm.state.keySeq;
4010 if (seq) {
4011 if (isModifierKey(name)) return "handled";
4012 stopSeq.set(50, function() {
4013 if (cm.state.keySeq == seq) {
4014 cm.state.keySeq = null;
4015 cm.display.input.reset();
4016 }
4017 });
4018 name = seq + " " + name;
4019 }
4020 var result = lookupKeyForEditor(cm, name, handle);
4021
4022 if (result == "multi")
4023 cm.state.keySeq = name;
4024 if (result == "handled")
4025 signalLater(cm, "keyHandled", cm, name, e);
4026
4027 if (result == "handled" || result == "multi") {
4028 e_preventDefault(e);
4029 restartBlink(cm);
4030 }
4031
4032 if (seq && !result && /\'$/.test(name)) {
4033 e_preventDefault(e);
4034 return true;
4035 }
4036 return !!result;
4037 }
4038
4039 // Handle a key from the keydown event.
4040 function handleKeyBinding(cm, e) {
4041 var name = keyName(e, true);
4042 if (!name) return false;
4043
4044 if (e.shiftKey && !cm.state.keySeq) {
4045 // First try to resolve full name (including 'Shift-'). Failing
4046 // that, see if there is a cursor-motion command (starting with
4047 // 'go') bound to the keyname without 'Shift-'.
4048 return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);})
4049 || dispatchKey(cm, name, e, function(b) {
4050 if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
4051 return doHandleBinding(cm, b);
4052 });
4053 } else {
4054 return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); });
4055 }
4056 }
4057
4058 // Handle a key from the keypress event
4059 function handleCharBinding(cm, e, ch) {
4060 return dispatchKey(cm, "'" + ch + "'", e,
4061 function(b) { return doHandleBinding(cm, b, true); });
4062 }
4063
4064 var lastStoppedKey = null;
4065 function onKeyDown(e) {
4066 var cm = this;
4067 cm.curOp.focus = activeElt();
4068 if (signalDOMEvent(cm, e)) return;
4069 // IE does strange things with escape.
4070 if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
4071 var code = e.keyCode;
4072 cm.display.shift = code == 16 || e.shiftKey;
4073 var handled = handleKeyBinding(cm, e);
4074 if (presto) {
4075 lastStoppedKey = handled ? code : null;
4076 // Opera has no cut event... we try to at least catch the key combo
4077 if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
4078 cm.replaceSelection("", null, "cut");
4079 }
4080
4081 // Turn mouse into crosshair when Alt is held on Mac.
4082 if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className))
4083 showCrossHair(cm);
4084 }
4085
4086 function showCrossHair(cm) {
4087 var lineDiv = cm.display.lineDiv;
4088 addClass(lineDiv, "CodeMirror-crosshair");
4089
4090 function up(e) {
4091 if (e.keyCode == 18 || !e.altKey) {
4092 rmClass(lineDiv, "CodeMirror-crosshair");
4093 off(document, "keyup", up);
4094 off(document, "mouseover", up);
4095 }
4096 }
4097 on(document, "keyup", up);
4098 on(document, "mouseover", up);
4099 }
4100
4101 function onKeyUp(e) {
4102 if (e.keyCode == 16) this.doc.sel.shift = false;
4103 signalDOMEvent(this, e);
4104 }
4105
4106 function onKeyPress(e) {
4107 var cm = this;
4108 if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
4109 var keyCode = e.keyCode, charCode = e.charCode;
4110 if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
4111 if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return;
4112 var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
4113 if (handleCharBinding(cm, e, ch)) return;
4114 cm.display.input.onKeyPress(e);
4115 }
4116
4117 // FOCUS/BLUR EVENTS
4118
4119 function delayBlurEvent(cm) {
4120 cm.state.delayingBlurEvent = true;
4121 setTimeout(function() {
4122 if (cm.state.delayingBlurEvent) {
4123 cm.state.delayingBlurEvent = false;
4124 onBlur(cm);
4125 }
4126 }, 100);
4127 }
4128
4129 function onFocus(cm) {
4130 if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false;
4131
4132 if (cm.options.readOnly == "nocursor") return;
4133 if (!cm.state.focused) {
4134 signal(cm, "focus", cm);
4135 cm.state.focused = true;
4136 addClass(cm.display.wrapper, "CodeMirror-focused");
4137 // This test prevents this from firing when a context
4138 // menu is closed (since the input reset would kill the
4139 // select-all detection hack)
4140 if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {
4141 cm.display.input.reset();
4142 if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730
4143 }
4144 cm.display.input.receivedFocus();
4145 }
4146 restartBlink(cm);
4147 }
4148 function onBlur(cm) {
4149 if (cm.state.delayingBlurEvent) return;
4150
4151 if (cm.state.focused) {
4152 signal(cm, "blur", cm);
4153 cm.state.focused = false;
4154 rmClass(cm.display.wrapper, "CodeMirror-focused");
4155 }
4156 clearInterval(cm.display.blinker);
4157 setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150);
4158 }
4159
4160 // CONTEXT MENU HANDLING
4161
4162 // To make the context menu work, we need to briefly unhide the
4163 // textarea (making it as unobtrusive as possible) to let the
4164 // right-click take effect on it.
4165 function onContextMenu(cm, e) {
4166 if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return;
4167 cm.display.input.onContextMenu(e);
4168 }
4169
4170 function contextMenuInGutter(cm, e) {
4171 if (!hasHandler(cm, "gutterContextMenu")) return false;
4172 return gutterEvent(cm, e, "gutterContextMenu", false, signal);
4173 }
4174
4175 // UPDATING
4176
4177 // Compute the position of the end of a change (its 'to' property
4178 // refers to the pre-change end).
4179 var changeEnd = CodeMirror.changeEnd = function(change) {
4180 if (!change.text) return change.to;
4181 return Pos(change.from.line + change.text.length - 1,
4182 lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
4183 };
4184
4185 // Adjust a position to refer to the post-change position of the
4186 // same text, or the end of the change if the change covers it.
4187 function adjustForChange(pos, change) {
4188 if (cmp(pos, change.from) < 0) return pos;
4189 if (cmp(pos, change.to) <= 0) return changeEnd(change);
4190
4191 var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
4192 if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch;
4193 return Pos(line, ch);
4194 }
4195
4196 function computeSelAfterChange(doc, change) {
4197 var out = [];
4198 for (var i = 0; i < doc.sel.ranges.length; i++) {
4199 var range = doc.sel.ranges[i];
4200 out.push(new Range(adjustForChange(range.anchor, change),
4201 adjustForChange(range.head, change)));
4202 }
4203 return normalizeSelection(out, doc.sel.primIndex);
4204 }
4205
4206 function offsetPos(pos, old, nw) {
4207 if (pos.line == old.line)
4208 return Pos(nw.line, pos.ch - old.ch + nw.ch);
4209 else
4210 return Pos(nw.line + (pos.line - old.line), pos.ch);
4211 }
4212
4213 // Used by replaceSelections to allow moving the selection to the
4214 // start or around the replaced test. Hint may be "start" or "around".
4215 function computeReplacedSel(doc, changes, hint) {
4216 var out = [];
4217 var oldPrev = Pos(doc.first, 0), newPrev = oldPrev;
4218 for (var i = 0; i < changes.length; i++) {
4219 var change = changes[i];
4220 var from = offsetPos(change.from, oldPrev, newPrev);
4221 var to = offsetPos(changeEnd(change), oldPrev, newPrev);
4222 oldPrev = change.to;
4223 newPrev = to;
4224 if (hint == "around") {
4225 var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0;
4226 out[i] = new Range(inv ? to : from, inv ? from : to);
4227 } else {
4228 out[i] = new Range(from, from);
4229 }
4230 }
4231 return new Selection(out, doc.sel.primIndex);
4232 }
4233
4234 // Allow "beforeChange" event handlers to influence a change
4235 function filterChange(doc, change, update) {
4236 var obj = {
4237 canceled: false,
4238 from: change.from,
4239 to: change.to,
4240 text: change.text,
4241 origin: change.origin,
4242 cancel: function() { this.canceled = true; }
4243 };
4244 if (update) obj.update = function(from, to, text, origin) {
4245 if (from) this.from = clipPos(doc, from);
4246 if (to) this.to = clipPos(doc, to);
4247 if (text) this.text = text;
4248 if (origin !== undefined) this.origin = origin;
4249 };
4250 signal(doc, "beforeChange", doc, obj);
4251 if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
4252
4253 if (obj.canceled) return null;
4254 return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
4255 }
4256
4257 // Apply a change to a document, and add it to the document's
4258 // history, and propagating it to all linked documents.
4259 function makeChange(doc, change, ignoreReadOnly) {
4260 if (doc.cm) {
4261 if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly);
4262 if (doc.cm.state.suppressEdits) return;
4263 }
4264
4265 if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
4266 change = filterChange(doc, change, true);
4267 if (!change) return;
4268 }
4269
4270 // Possibly split or suppress the update based on the presence
4271 // of read-only spans in its range.
4272 var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
4273 if (split) {
4274 for (var i = split.length - 1; i >= 0; --i)
4275 makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text});
4276 } else {
4277 makeChangeInner(doc, change);
4278 }
4279 }
4280
4281 function makeChangeInner(doc, change) {
4282 if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return;
4283 var selAfter = computeSelAfterChange(doc, change);
4284 addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
4285
4286 makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
4287 var rebased = [];
4288
4289 linkedDocs(doc, function(doc, sharedHist) {
4290 if (!sharedHist && indexOf(rebased, doc.history) == -1) {
4291 rebaseHist(doc.history, change);
4292 rebased.push(doc.history);
4293 }
4294 makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
4295 });
4296 }
4297
4298 // Revert a change stored in a document's history.
4299 function makeChangeFromHistory(doc, type, allowSelectionOnly) {
4300 if (doc.cm && doc.cm.state.suppressEdits) return;
4301
4302 var hist = doc.history, event, selAfter = doc.sel;
4303 var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done;
4304
4305 // Verify that there is a useable event (so that ctrl-z won't
4306 // needlessly clear selection events)
4307 for (var i = 0; i < source.length; i++) {
4308 event = source[i];
4309 if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)
4310 break;
4311 }
4312 if (i == source.length) return;
4313 hist.lastOrigin = hist.lastSelOrigin = null;
4314
4315 for (;;) {
4316 event = source.pop();
4317 if (event.ranges) {
4318 pushSelectionToHistory(event, dest);
4319 if (allowSelectionOnly && !event.equals(doc.sel)) {
4320 setSelection(doc, event, {clearRedo: false});
4321 return;
4322 }
4323 selAfter = event;
4324 }
4325 else break;
4326 }
4327
4328 // Build up a reverse change object to add to the opposite history
4329 // stack (redo when undoing, and vice versa).
4330 var antiChanges = [];
4331 pushSelectionToHistory(selAfter, dest);
4332 dest.push({changes: antiChanges, generation: hist.generation});
4333 hist.generation = event.generation || ++hist.maxGeneration;
4334
4335 var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange");
4336
4337 for (var i = event.changes.length - 1; i >= 0; --i) {
4338 var change = event.changes[i];
4339 change.origin = type;
4340 if (filter && !filterChange(doc, change, false)) {
4341 source.length = 0;
4342 return;
4343 }
4344
4345 antiChanges.push(historyChangeFromChange(doc, change));
4346
4347 var after = i ? computeSelAfterChange(doc, change) : lst(source);
4348 makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
4349 if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
4350 var rebased = [];
4351
4352 // Propagate to the linked documents
4353 linkedDocs(doc, function(doc, sharedHist) {
4354 if (!sharedHist && indexOf(rebased, doc.history) == -1) {
4355 rebaseHist(doc.history, change);
4356 rebased.push(doc.history);
4357 }
4358 makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
4359 });
4360 }
4361 }
4362
4363 // Sub-views need their line numbers shifted when text is added
4364 // above or below them in the parent document.
4365 function shiftDoc(doc, distance) {
4366 if (distance == 0) return;
4367 doc.first += distance;
4368 doc.sel = new Selection(map(doc.sel.ranges, function(range) {
4369 return new Range(Pos(range.anchor.line + distance, range.anchor.ch),
4370 Pos(range.head.line + distance, range.head.ch));
4371 }), doc.sel.primIndex);
4372 if (doc.cm) {
4373 regChange(doc.cm, doc.first, doc.first - distance, distance);
4374 for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
4375 regLineChange(doc.cm, l, "gutter");
4376 }
4377 }
4378
4379 // More lower-level change function, handling only a single document
4380 // (not linked ones).
4381 function makeChangeSingleDoc(doc, change, selAfter, spans) {
4382 if (doc.cm && !doc.cm.curOp)
4383 return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
4384
4385 if (change.to.line < doc.first) {
4386 shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
4387 return;
4388 }
4389 if (change.from.line > doc.lastLine()) return;
4390
4391 // Clip the change to the size of this doc
4392 if (change.from.line < doc.first) {
4393 var shift = change.text.length - 1 - (doc.first - change.from.line);
4394 shiftDoc(doc, shift);
4395 change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
4396 text: [lst(change.text)], origin: change.origin};
4397 }
4398 var last = doc.lastLine();
4399 if (change.to.line > last) {
4400 change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
4401 text: [change.text[0]], origin: change.origin};
4402 }
4403
4404 change.removed = getBetween(doc, change.from, change.to);
4405
4406 if (!selAfter) selAfter = computeSelAfterChange(doc, change);
4407 if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
4408 else updateDoc(doc, change, spans);
4409 setSelectionNoUndo(doc, selAfter, sel_dontScroll);
4410 }
4411
4412 // Handle the interaction of a change to a document with the editor
4413 // that this document is part of.
4414 function makeChangeSingleDocInEditor(cm, change, spans) {
4415 var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
4416
4417 var recomputeMaxLength = false, checkWidthStart = from.line;
4418 if (!cm.options.lineWrapping) {
4419 checkWidthStart = lineNo(visualLine(getLine(doc, from.line)));
4420 doc.iter(checkWidthStart, to.line + 1, function(line) {
4421 if (line == display.maxLine) {
4422 recomputeMaxLength = true;
4423 return true;
4424 }
4425 });
4426 }
4427
4428 if (doc.sel.contains(change.from, change.to) > -1)
4429 signalCursorActivity(cm);
4430
4431 updateDoc(doc, change, spans, estimateHeight(cm));
4432
4433 if (!cm.options.lineWrapping) {
4434 doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
4435 var len = lineLength(line);
4436 if (len > display.maxLineLength) {
4437 display.maxLine = line;
4438 display.maxLineLength = len;
4439 display.maxLineChanged = true;
4440 recomputeMaxLength = false;
4441 }
4442 });
4443 if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
4444 }
4445
4446 // Adjust frontier, schedule worker
4447 doc.frontier = Math.min(doc.frontier, from.line);
4448 startWorker(cm, 400);
4449
4450 var lendiff = change.text.length - (to.line - from.line) - 1;
4451 // Remember that these lines changed, for updating the display
4452 if (change.full)
4453 regChange(cm);
4454 else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))
4455 regLineChange(cm, from.line, "text");
4456 else
4457 regChange(cm, from.line, to.line + 1, lendiff);
4458
4459 var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change");
4460 if (changeHandler || changesHandler) {
4461 var obj = {
4462 from: from, to: to,
4463 text: change.text,
4464 removed: change.removed,
4465 origin: change.origin
4466 };
4467 if (changeHandler) signalLater(cm, "change", cm, obj);
4468 if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
4469 }
4470 cm.display.selForContextMenu = null;
4471 }
4472
4473 function replaceRange(doc, code, from, to, origin) {
4474 if (!to) to = from;
4475 if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; }
4476 if (typeof code == "string") code = splitLines(code);
4477 makeChange(doc, {from: from, to: to, text: code, origin: origin});
4478 }
4479
4480 // SCROLLING THINGS INTO VIEW
4481
4482 // If an editor sits on the top or bottom of the window, partially
4483 // scrolled out of view, this ensures that the cursor is visible.
4484 function maybeScrollWindow(cm, coords) {
4485 if (signalDOMEvent(cm, "scrollCursorIntoView")) return;
4486
4487 var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null;
4488 if (coords.top + box.top < 0) doScroll = true;
4489 else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
4490 if (doScroll != null && !phantom) {
4491 var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
4492 (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " +
4493 (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " +
4494 coords.left + "px; width: 2px;");
4495 cm.display.lineSpace.appendChild(scrollNode);
4496 scrollNode.scrollIntoView(doScroll);
4497 cm.display.lineSpace.removeChild(scrollNode);
4498 }
4499 }
4500
4501 // Scroll a given position into view (immediately), verifying that
4502 // it actually became visible (as line heights are accurately
4503 // measured, the position of something may 'drift' during drawing).
4504 function scrollPosIntoView(cm, pos, end, margin) {
4505 if (margin == null) margin = 0;
4506 for (var limit = 0; limit < 5; limit++) {
4507 var changed = false, coords = cursorCoords(cm, pos);
4508 var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
4509 var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
4510 Math.min(coords.top, endCoords.top) - margin,
4511 Math.max(coords.left, endCoords.left),
4512 Math.max(coords.bottom, endCoords.bottom) + margin);
4513 var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
4514 if (scrollPos.scrollTop != null) {
4515 setScrollTop(cm, scrollPos.scrollTop);
4516 if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
4517 }
4518 if (scrollPos.scrollLeft != null) {
4519 setScrollLeft(cm, scrollPos.scrollLeft);
4520 if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
4521 }
4522 if (!changed) break;
4523 }
4524 return coords;
4525 }
4526
4527 // Scroll a given set of coordinates into view (immediately).
4528 function scrollIntoView(cm, x1, y1, x2, y2) {
4529 var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
4530 if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
4531 if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
4532 }
4533
4534 // Calculate a new scroll position needed to scroll the given
4535 // rectangle into view. Returns an object with scrollTop and
4536 // scrollLeft properties. When these are undefined, the
4537 // vertical/horizontal position does not need to be adjusted.
4538 function calculateScrollPos(cm, x1, y1, x2, y2) {
4539 var display = cm.display, snapMargin = textHeight(cm.display);
4540 if (y1 < 0) y1 = 0;
4541 var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
4542 var screen = displayHeight(cm), result = {};
4543 if (y2 - y1 > screen) y2 = y1 + screen;
4544 var docBottom = cm.doc.height + paddingVert(display);
4545 var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
4546 if (y1 < screentop) {
4547 result.scrollTop = atTop ? 0 : y1;
4548 } else if (y2 > screentop + screen) {
4549 var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
4550 if (newTop != screentop) result.scrollTop = newTop;
4551 }
4552
4553 var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
4554 var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0);
4555 var tooWide = x2 - x1 > screenw;
4556 if (tooWide) x2 = x1 + screenw;
4557 if (x1 < 10)
4558 result.scrollLeft = 0;
4559 else if (x1 < screenleft)
4560 result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
4561 else if (x2 > screenw + screenleft - 3)
4562 result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
4563 return result;
4564 }
4565
4566 // Store a relative adjustment to the scroll position in the current
4567 // operation (to be applied when the operation finishes).
4568 function addToScrollPos(cm, left, top) {
4569 if (left != null || top != null) resolveScrollToPos(cm);
4570 if (left != null)
4571 cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left;
4572 if (top != null)
4573 cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top;
4574 }
4575
4576 // Make sure that at the end of the operation the current cursor is
4577 // shown.
4578 function ensureCursorVisible(cm) {
4579 resolveScrollToPos(cm);
4580 var cur = cm.getCursor(), from = cur, to = cur;
4581 if (!cm.options.lineWrapping) {
4582 from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur;
4583 to = Pos(cur.line, cur.ch + 1);
4584 }
4585 cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true};
4586 }
4587
4588 // When an operation has its scrollToPos property set, and another
4589 // scroll action is applied before the end of the operation, this
4590 // 'simulates' scrolling that position into view in a cheap way, so
4591 // that the effect of intermediate scroll commands is not ignored.
4592 function resolveScrollToPos(cm) {
4593 var range = cm.curOp.scrollToPos;
4594 if (range) {
4595 cm.curOp.scrollToPos = null;
4596 var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to);
4597 var sPos = calculateScrollPos(cm, Math.min(from.left, to.left),
4598 Math.min(from.top, to.top) - range.margin,
4599 Math.max(from.right, to.right),
4600 Math.max(from.bottom, to.bottom) + range.margin);
4601 cm.scrollTo(sPos.scrollLeft, sPos.scrollTop);
4602 }
4603 }
4604
4605 // API UTILITIES
4606
4607 // Indent the given line. The how parameter can be "smart",
4608 // "add"/null, "subtract", or "prev". When aggressive is false
4609 // (typically set to true for forced single-line indents), empty
4610 // lines are not indented, and places where the mode returns Pass
4611 // are left alone.
4612 function indentLine(cm, n, how, aggressive) {
4613 var doc = cm.doc, state;
4614 if (how == null) how = "add";
4615 if (how == "smart") {
4616 // Fall back to "prev" when the mode doesn't have an indentation
4617 // method.
4618 if (!doc.mode.indent) how = "prev";
4619 else state = getStateBefore(cm, n);
4620 }
4621
4622 var tabSize = cm.options.tabSize;
4623 var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
4624 if (line.stateAfter) line.stateAfter = null;
4625 var curSpaceString = line.text.match(/^\s*/)[0], indentation;
4626 if (!aggressive && !/\S/.test(line.text)) {
4627 indentation = 0;
4628 how = "not";
4629 } else if (how == "smart") {
4630 indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
4631 if (indentation == Pass || indentation > 150) {
4632 if (!aggressive) return;
4633 how = "prev";
4634 }
4635 }
4636 if (how == "prev") {
4637 if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
4638 else indentation = 0;
4639 } else if (how == "add") {
4640 indentation = curSpace + cm.options.indentUnit;
4641 } else if (how == "subtract") {
4642 indentation = curSpace - cm.options.indentUnit;
4643 } else if (typeof how == "number") {
4644 indentation = curSpace + how;
4645 }
4646 indentation = Math.max(0, indentation);
4647
4648 var indentString = "", pos = 0;
4649 if (cm.options.indentWithTabs)
4650 for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
4651 if (pos < indentation) indentString += spaceStr(indentation - pos);
4652
4653 if (indentString != curSpaceString) {
4654 replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
4655 line.stateAfter = null;
4656 return true;
4657 } else {
4658 // Ensure that, if the cursor was in the whitespace at the start
4659 // of the line, it is moved to the end of that space.
4660 for (var i = 0; i < doc.sel.ranges.length; i++) {
4661 var range = doc.sel.ranges[i];
4662 if (range.head.line == n && range.head.ch < curSpaceString.length) {
4663 var pos = Pos(n, curSpaceString.length);
4664 replaceOneSelection(doc, i, new Range(pos, pos));
4665 break;
4666 }
4667 }
4668 }
4669 }
4670
4671 // Utility for applying a change to a line by handle or number,
4672 // returning the number and optionally registering the line as
4673 // changed.
4674 function changeLine(doc, handle, changeType, op) {
4675 var no = handle, line = handle;
4676 if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
4677 else no = lineNo(handle);
4678 if (no == null) return null;
4679 if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
4680 return line;
4681 }
4682
4683 // Helper for deleting text near the selection(s), used to implement
4684 // backspace, delete, and similar functionality.
4685 function deleteNearSelection(cm, compute) {
4686 var ranges = cm.doc.sel.ranges, kill = [];
4687 // Build up a set of ranges to kill first, merging overlapping
4688 // ranges.
4689 for (var i = 0; i < ranges.length; i++) {
4690 var toKill = compute(ranges[i]);
4691 while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) {
4692 var replaced = kill.pop();
4693 if (cmp(replaced.from, toKill.from) < 0) {
4694 toKill.from = replaced.from;
4695 break;
4696 }
4697 }
4698 kill.push(toKill);
4699 }
4700 // Next, remove those actual ranges.
4701 runInOp(cm, function() {
4702 for (var i = kill.length - 1; i >= 0; i--)
4703 replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete");
4704 ensureCursorVisible(cm);
4705 });
4706 }
4707
4708 // Used for horizontal relative motion. Dir is -1 or 1 (left or
4709 // right), unit can be "char", "column" (like char, but doesn't
4710 // cross line boundaries), "word" (across next word), or "group" (to
4711 // the start of next group of word or non-word-non-whitespace
4712 // chars). The visually param controls whether, in right-to-left
4713 // text, direction 1 means to move towards the next index in the
4714 // string, or towards the character to the right of the current
4715 // position. The resulting position will have a hitSide=true
4716 // property if it reached the end of the document.
4717 function findPosH(doc, pos, dir, unit, visually) {
4718 var line = pos.line, ch = pos.ch, origDir = dir;
4719 var lineObj = getLine(doc, line);
4720 var possible = true;
4721 function findNextLine() {
4722 var l = line + dir;
4723 if (l < doc.first || l >= doc.first + doc.size) return (possible = false);
4724 line = l;
4725 return lineObj = getLine(doc, l);
4726 }
4727 function moveOnce(boundToLine) {
4728 var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
4729 if (next == null) {
4730 if (!boundToLine && findNextLine()) {
4731 if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
4732 else ch = dir < 0 ? lineObj.text.length : 0;
4733 } else return (possible = false);
4734 } else ch = next;
4735 return true;
4736 }
4737
4738 if (unit == "char") moveOnce();
4739 else if (unit == "column") moveOnce(true);
4740 else if (unit == "word" || unit == "group") {
4741 var sawType = null, group = unit == "group";
4742 var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
4743 for (var first = true;; first = false) {
4744 if (dir < 0 && !moveOnce(!first)) break;
4745 var cur = lineObj.text.charAt(ch) || "\n";
4746 var type = isWordChar(cur, helper) ? "w"
4747 : group && cur == "\n" ? "n"
4748 : !group || /\s/.test(cur) ? null
4749 : "p";
4750 if (group && !first && !type) type = "s";
4751 if (sawType && sawType != type) {
4752 if (dir < 0) {dir = 1; moveOnce();}
4753 break;
4754 }
4755
4756 if (type) sawType = type;
4757 if (dir > 0 && !moveOnce(!first)) break;
4758 }
4759 }
4760 var result = skipAtomic(doc, Pos(line, ch), origDir, true);
4761 if (!possible) result.hitSide = true;
4762 return result;
4763 }
4764
4765 // For relative vertical movement. Dir may be -1 or 1. Unit can be
4766 // "page" or "line". The resulting position will have a hitSide=true
4767 // property if it reached the end of the document.
4768 function findPosV(cm, pos, dir, unit) {
4769 var doc = cm.doc, x = pos.left, y;
4770 if (unit == "page") {
4771 var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
4772 y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
4773 } else if (unit == "line") {
4774 y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
4775 }
4776 for (;;) {
4777 var target = coordsChar(cm, x, y);
4778 if (!target.outside) break;
4779 if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
4780 y += dir * 5;
4781 }
4782 return target;
4783 }
4784
4785 // EDITOR METHODS
4786
4787 // The publicly visible API. Note that methodOp(f) means
4788 // 'wrap f in an operation, performed on its `this` parameter'.
4789
4790 // This is not the complete set of editor methods. Most of the
4791 // methods defined on the Doc type are also injected into
4792 // CodeMirror.prototype, for backwards compatibility and
4793 // convenience.
4794
4795 CodeMirror.prototype = {
4796 constructor: CodeMirror,
4797 focus: function(){window.focus(); this.display.input.focus();},
4798
4799 setOption: function(option, value) {
4800 var options = this.options, old = options[option];
4801 if (options[option] == value && option != "mode") return;
4802 options[option] = value;
4803 if (optionHandlers.hasOwnProperty(option))
4804 operation(this, optionHandlers[option])(this, value, old);
4805 },
4806
4807 getOption: function(option) {return this.options[option];},
4808 getDoc: function() {return this.doc;},
4809
4810 addKeyMap: function(map, bottom) {
4811 this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map));
4812 },
4813 removeKeyMap: function(map) {
4814 var maps = this.state.keyMaps;
4815 for (var i = 0; i < maps.length; ++i)
4816 if (maps[i] == map || maps[i].name == map) {
4817 maps.splice(i, 1);
4818 return true;
4819 }
4820 },
4821
4822 addOverlay: methodOp(function(spec, options) {
4823 var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
4824 if (mode.startState) throw new Error("Overlays may not be stateful.");
4825 this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
4826 this.state.modeGen++;
4827 regChange(this);
4828 }),
4829 removeOverlay: methodOp(function(spec) {
4830 var overlays = this.state.overlays;
4831 for (var i = 0; i < overlays.length; ++i) {
4832 var cur = overlays[i].modeSpec;
4833 if (cur == spec || typeof spec == "string" && cur.name == spec) {
4834 overlays.splice(i, 1);
4835 this.state.modeGen++;
4836 regChange(this);
4837 return;
4838 }
4839 }
4840 }),
4841
4842 indentLine: methodOp(function(n, dir, aggressive) {
4843 if (typeof dir != "string" && typeof dir != "number") {
4844 if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
4845 else dir = dir ? "add" : "subtract";
4846 }
4847 if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
4848 }),
4849 indentSelection: methodOp(function(how) {
4850 var ranges = this.doc.sel.ranges, end = -1;
4851 for (var i = 0; i < ranges.length; i++) {
4852 var range = ranges[i];
4853 if (!range.empty()) {
4854 var from = range.from(), to = range.to();
4855 var start = Math.max(end, from.line);
4856 end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
4857 for (var j = start; j < end; ++j)
4858 indentLine(this, j, how);
4859 var newRanges = this.doc.sel.ranges;
4860 if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
4861 replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll);
4862 } else if (range.head.line > end) {
4863 indentLine(this, range.head.line, how, true);
4864 end = range.head.line;
4865 if (i == this.doc.sel.primIndex) ensureCursorVisible(this);
4866 }
4867 }
4868 }),
4869
4870 // Fetch the parser token for a given character. Useful for hacks
4871 // that want to inspect the mode state (say, for completion).
4872 getTokenAt: function(pos, precise) {
4873 return takeToken(this, pos, precise);
4874 },
4875
4876 getLineTokens: function(line, precise) {
4877 return takeToken(this, Pos(line), precise, true);
4878 },
4879
4880 getTokenTypeAt: function(pos) {
4881 pos = clipPos(this.doc, pos);
4882 var styles = getLineStyles(this, getLine(this.doc, pos.line));
4883 var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
4884 var type;
4885 if (ch == 0) type = styles[2];
4886 else for (;;) {
4887 var mid = (before + after) >> 1;
4888 if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
4889 else if (styles[mid * 2 + 1] < ch) before = mid + 1;
4890 else { type = styles[mid * 2 + 2]; break; }
4891 }
4892 var cut = type ? type.indexOf("cm-overlay ") : -1;
4893 return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1);
4894 },
4895
4896 getModeAt: function(pos) {
4897 var mode = this.doc.mode;
4898 if (!mode.innerMode) return mode;
4899 return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
4900 },
4901
4902 getHelper: function(pos, type) {
4903 return this.getHelpers(pos, type)[0];
4904 },
4905
4906 getHelpers: function(pos, type) {
4907 var found = [];
4908 if (!helpers.hasOwnProperty(type)) return found;
4909 var help = helpers[type], mode = this.getModeAt(pos);
4910 if (typeof mode[type] == "string") {
4911 if (help[mode[type]]) found.push(help[mode[type]]);
4912 } else if (mode[type]) {
4913 for (var i = 0; i < mode[type].length; i++) {
4914 var val = help[mode[type][i]];
4915 if (val) found.push(val);
4916 }
4917 } else if (mode.helperType && help[mode.helperType]) {
4918 found.push(help[mode.helperType]);
4919 } else if (help[mode.name]) {
4920 found.push(help[mode.name]);
4921 }
4922 for (var i = 0; i < help._global.length; i++) {
4923 var cur = help._global[i];
4924 if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
4925 found.push(cur.val);
4926 }
4927 return found;
4928 },
4929
4930 getStateAfter: function(line, precise) {
4931 var doc = this.doc;
4932 line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
4933 return getStateBefore(this, line + 1, precise);
4934 },
4935
4936 cursorCoords: function(start, mode) {
4937 var pos, range = this.doc.sel.primary();
4938 if (start == null) pos = range.head;
4939 else if (typeof start == "object") pos = clipPos(this.doc, start);
4940 else pos = start ? range.from() : range.to();
4941 return cursorCoords(this, pos, mode || "page");
4942 },
4943
4944 charCoords: function(pos, mode) {
4945 return charCoords(this, clipPos(this.doc, pos), mode || "page");
4946 },
4947
4948 coordsChar: function(coords, mode) {
4949 coords = fromCoordSystem(this, coords, mode || "page");
4950 return coordsChar(this, coords.left, coords.top);
4951 },
4952
4953 lineAtHeight: function(height, mode) {
4954 height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
4955 return lineAtHeight(this.doc, height + this.display.viewOffset);
4956 },
4957 heightAtLine: function(line, mode) {
4958 var end = false, lineObj;
4959 if (typeof line == "number") {
4960 var last = this.doc.first + this.doc.size - 1;
4961 if (line < this.doc.first) line = this.doc.first;
4962 else if (line > last) { line = last; end = true; }
4963 lineObj = getLine(this.doc, line);
4964 } else {
4965 lineObj = line;
4966 }
4967 return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top +
4968 (end ? this.doc.height - heightAtLine(lineObj) : 0);
4969 },
4970
4971 defaultTextHeight: function() { return textHeight(this.display); },
4972 defaultCharWidth: function() { return charWidth(this.display); },
4973
4974 setGutterMarker: methodOp(function(line, gutterID, value) {
4975 return changeLine(this.doc, line, "gutter", function(line) {
4976 var markers = line.gutterMarkers || (line.gutterMarkers = {});
4977 markers[gutterID] = value;
4978 if (!value && isEmpty(markers)) line.gutterMarkers = null;
4979 return true;
4980 });
4981 }),
4982
4983 clearGutter: methodOp(function(gutterID) {
4984 var cm = this, doc = cm.doc, i = doc.first;
4985 doc.iter(function(line) {
4986 if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
4987 line.gutterMarkers[gutterID] = null;
4988 regLineChange(cm, i, "gutter");
4989 if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
4990 }
4991 ++i;
4992 });
4993 }),
4994
4995 lineInfo: function(line) {
4996 if (typeof line == "number") {
4997 if (!isLine(this.doc, line)) return null;
4998 var n = line;
4999 line = getLine(this.doc, line);
5000 if (!line) return null;
5001 } else {
5002 var n = lineNo(line);
5003 if (n == null) return null;
5004 }
5005 return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
5006 textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
5007 widgets: line.widgets};
5008 },
5009
5010 getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};},
5011
5012 addWidget: function(pos, node, scroll, vert, horiz) {
5013 var display = this.display;
5014 pos = cursorCoords(this, clipPos(this.doc, pos));
5015 var top = pos.bottom, left = pos.left;
5016 node.style.position = "absolute";
5017 node.setAttribute("cm-ignore-events", "true");
5018 this.display.input.setUneditable(node);
5019 display.sizer.appendChild(node);
5020 if (vert == "over") {
5021 top = pos.top;
5022 } else if (vert == "above" || vert == "near") {
5023 var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
5024 hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
5025 // Default to positioning above (if specified and possible); otherwise default to positioning below
5026 if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
5027 top = pos.top - node.offsetHeight;
5028 else if (pos.bottom + node.offsetHeight <= vspace)
5029 top = pos.bottom;
5030 if (left + node.offsetWidth > hspace)
5031 left = hspace - node.offsetWidth;
5032 }
5033 node.style.top = top + "px";
5034 node.style.left = node.style.right = "";
5035 if (horiz == "right") {
5036 left = display.sizer.clientWidth - node.offsetWidth;
5037 node.style.right = "0px";
5038 } else {
5039 if (horiz == "left") left = 0;
5040 else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
5041 node.style.left = left + "px";
5042 }
5043 if (scroll)
5044 scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
5045 },
5046
5047 triggerOnKeyDown: methodOp(onKeyDown),
5048 triggerOnKeyPress: methodOp(onKeyPress),
5049 triggerOnKeyUp: onKeyUp,
5050
5051 execCommand: function(cmd) {
5052 if (commands.hasOwnProperty(cmd))
5053 return commands[cmd](this);
5054 },
5055
5056 findPosH: function(from, amount, unit, visually) {
5057 var dir = 1;
5058 if (amount < 0) { dir = -1; amount = -amount; }
5059 for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
5060 cur = findPosH(this.doc, cur, dir, unit, visually);
5061 if (cur.hitSide) break;
5062 }
5063 return cur;
5064 },
5065
5066 moveH: methodOp(function(dir, unit) {
5067 var cm = this;
5068 cm.extendSelectionsBy(function(range) {
5069 if (cm.display.shift || cm.doc.extend || range.empty())
5070 return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually);
5071 else
5072 return dir < 0 ? range.from() : range.to();
5073 }, sel_move);
5074 }),
5075
5076 deleteH: methodOp(function(dir, unit) {
5077 var sel = this.doc.sel, doc = this.doc;
5078 if (sel.somethingSelected())
5079 doc.replaceSelection("", null, "+delete");
5080 else
5081 deleteNearSelection(this, function(range) {
5082 var other = findPosH(doc, range.head, dir, unit, false);
5083 return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other};
5084 });
5085 }),
5086
5087 findPosV: function(from, amount, unit, goalColumn) {
5088 var dir = 1, x = goalColumn;
5089 if (amount < 0) { dir = -1; amount = -amount; }
5090 for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
5091 var coords = cursorCoords(this, cur, "div");
5092 if (x == null) x = coords.left;
5093 else coords.left = x;
5094 cur = findPosV(this, coords, dir, unit);
5095 if (cur.hitSide) break;
5096 }
5097 return cur;
5098 },
5099
5100 moveV: methodOp(function(dir, unit) {
5101 var cm = this, doc = this.doc, goals = [];
5102 var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected();
5103 doc.extendSelectionsBy(function(range) {
5104 if (collapse)
5105 return dir < 0 ? range.from() : range.to();
5106 var headPos = cursorCoords(cm, range.head, "div");
5107 if (range.goalColumn != null) headPos.left = range.goalColumn;
5108 goals.push(headPos.left);
5109 var pos = findPosV(cm, headPos, dir, unit);
5110 if (unit == "page" && range == doc.sel.primary())
5111 addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top);
5112 return pos;
5113 }, sel_move);
5114 if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++)
5115 doc.sel.ranges[i].goalColumn = goals[i];
5116 }),
5117
5118 // Find the word at the given position (as returned by coordsChar).
5119 findWordAt: function(pos) {
5120 var doc = this.doc, line = getLine(doc, pos.line).text;
5121 var start = pos.ch, end = pos.ch;
5122 if (line) {
5123 var helper = this.getHelper(pos, "wordChars");
5124 if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
5125 var startChar = line.charAt(start);
5126 var check = isWordChar(startChar, helper)
5127 ? function(ch) { return isWordChar(ch, helper); }
5128 : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
5129 : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
5130 while (start > 0 && check(line.charAt(start - 1))) --start;
5131 while (end < line.length && check(line.charAt(end))) ++end;
5132 }
5133 return new Range(Pos(pos.line, start), Pos(pos.line, end));
5134 },
5135
5136 toggleOverwrite: function(value) {
5137 if (value != null && value == this.state.overwrite) return;
5138 if (this.state.overwrite = !this.state.overwrite)
5139 addClass(this.display.cursorDiv, "CodeMirror-overwrite");
5140 else
5141 rmClass(this.display.cursorDiv, "CodeMirror-overwrite");
5142
5143 signal(this, "overwriteToggle", this, this.state.overwrite);
5144 },
5145 hasFocus: function() { return this.display.input.getField() == activeElt(); },
5146
5147 scrollTo: methodOp(function(x, y) {
5148 if (x != null || y != null) resolveScrollToPos(this);
5149 if (x != null) this.curOp.scrollLeft = x;
5150 if (y != null) this.curOp.scrollTop = y;
5151 }),
5152 getScrollInfo: function() {
5153 var scroller = this.display.scroller;
5154 return {left: scroller.scrollLeft, top: scroller.scrollTop,
5155 height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,
5156 width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,
5157 clientHeight: displayHeight(this), clientWidth: displayWidth(this)};
5158 },
5159
5160 scrollIntoView: methodOp(function(range, margin) {
5161 if (range == null) {
5162 range = {from: this.doc.sel.primary().head, to: null};
5163 if (margin == null) margin = this.options.cursorScrollMargin;
5164 } else if (typeof range == "number") {
5165 range = {from: Pos(range, 0), to: null};
5166 } else if (range.from == null) {
5167 range = {from: range, to: null};
5168 }
5169 if (!range.to) range.to = range.from;
5170 range.margin = margin || 0;
5171
5172 if (range.from.line != null) {
5173 resolveScrollToPos(this);
5174 this.curOp.scrollToPos = range;
5175 } else {
5176 var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left),
5177 Math.min(range.from.top, range.to.top) - range.margin,
5178 Math.max(range.from.right, range.to.right),
5179 Math.max(range.from.bottom, range.to.bottom) + range.margin);
5180 this.scrollTo(sPos.scrollLeft, sPos.scrollTop);
5181 }
5182 }),
5183
5184 setSize: methodOp(function(width, height) {
5185 var cm = this;
5186 function interpret(val) {
5187 return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
5188 }
5189 if (width != null) cm.display.wrapper.style.width = interpret(width);
5190 if (height != null) cm.display.wrapper.style.height = interpret(height);
5191 if (cm.options.lineWrapping) clearLineMeasurementCache(this);
5192 var lineNo = cm.display.viewFrom;
5193 cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
5194 if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
5195 if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
5196 ++lineNo;
5197 });
5198 cm.curOp.forceUpdate = true;
5199 signal(cm, "refresh", this);
5200 }),
5201
5202 operation: function(f){return runInOp(this, f);},
5203
5204 refresh: methodOp(function() {
5205 var oldHeight = this.display.cachedTextHeight;
5206 regChange(this);
5207 this.curOp.forceUpdate = true;
5208 clearCaches(this);
5209 this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop);
5210 updateGutterSpace(this);
5211 if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)
5212 estimateLineHeights(this);
5213 signal(this, "refresh", this);
5214 }),
5215
5216 swapDoc: methodOp(function(doc) {
5217 var old = this.doc;
5218 old.cm = null;
5219 attachDoc(this, doc);
5220 clearCaches(this);
5221 this.display.input.reset();
5222 this.scrollTo(doc.scrollLeft, doc.scrollTop);
5223 this.curOp.forceScroll = true;
5224 signalLater(this, "swapDoc", this, old);
5225 return old;
5226 }),
5227
5228 getInputField: function(){return this.display.input.getField();},
5229 getWrapperElement: function(){return this.display.wrapper;},
5230 getScrollerElement: function(){return this.display.scroller;},
5231 getGutterElement: function(){return this.display.gutters;}
5232 };
5233 eventMixin(CodeMirror);
5234
5235 // OPTION DEFAULTS
5236
5237 // The default configuration options.
5238 var defaults = CodeMirror.defaults = {};
5239 // Functions to run when options are changed.
5240 var optionHandlers = CodeMirror.optionHandlers = {};
5241
5242 function option(name, deflt, handle, notOnInit) {
5243 CodeMirror.defaults[name] = deflt;
5244 if (handle) optionHandlers[name] =
5245 notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
5246 }
5247
5248 // Passed to option handlers when there is no old value.
5249 var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
5250
5251 // These two are, on init, called from the constructor because they
5252 // have to be initialized before the editor can start at all.
5253 option("value", "", function(cm, val) {
5254 cm.setValue(val);
5255 }, true);
5256 option("mode", null, function(cm, val) {
5257 cm.doc.modeOption = val;
5258 loadMode(cm);
5259 }, true);
5260
5261 option("indentUnit", 2, loadMode, true);
5262 option("indentWithTabs", false);
5263 option("smartIndent", true);
5264 option("tabSize", 4, function(cm) {
5265 resetModeState(cm);
5266 clearCaches(cm);
5267 regChange(cm);
5268 }, true);
5269 option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) {
5270 cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
5271 if (old != CodeMirror.Init) cm.refresh();
5272 });
5273 option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
5274 option("electricChars", true);
5275 option("inputStyle", mobile ? "contenteditable" : "textarea", function() {
5276 throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME
5277 }, true);
5278 option("rtlMoveVisually", !windows);
5279 option("wholeLineUpdateBefore", true);
5280
5281 option("theme", "default", function(cm) {
5282 themeChanged(cm);
5283 guttersChanged(cm);
5284 }, true);
5285 option("keyMap", "default", function(cm, val, old) {
5286 var next = getKeyMap(val);
5287 var prev = old != CodeMirror.Init && getKeyMap(old);
5288 if (prev && prev.detach) prev.detach(cm, next);
5289 if (next.attach) next.attach(cm, prev || null);
5290 });
5291 option("extraKeys", null);
5292
5293 option("lineWrapping", false, wrappingChanged, true);
5294 option("gutters", [], function(cm) {
5295 setGuttersForLineNumbers(cm.options);
5296 guttersChanged(cm);
5297 }, true);
5298 option("fixedGutter", true, function(cm, val) {
5299 cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
5300 cm.refresh();
5301 }, true);
5302 option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true);
5303 option("scrollbarStyle", "native", function(cm) {
5304 initScrollbars(cm);
5305 updateScrollbars(cm);
5306 cm.display.scrollbars.setScrollTop(cm.doc.scrollTop);
5307 cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft);
5308 }, true);
5309 option("lineNumbers", false, function(cm) {
5310 setGuttersForLineNumbers(cm.options);
5311 guttersChanged(cm);
5312 }, true);
5313 option("firstLineNumber", 1, guttersChanged, true);
5314 option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
5315 option("showCursorWhenSelecting", false, updateSelection, true);
5316
5317 option("resetSelectionOnContextMenu", true);
5318 option("lineWiseCopyCut", true);
5319
5320 option("readOnly", false, function(cm, val) {
5321 if (val == "nocursor") {
5322 onBlur(cm);
5323 cm.display.input.blur();
5324 cm.display.disabled = true;
5325 } else {
5326 cm.display.disabled = false;
5327 if (!val) cm.display.input.reset();
5328 }
5329 });
5330 option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true);
5331 option("dragDrop", true, dragDropChanged);
5332
5333 option("cursorBlinkRate", 530);
5334 option("cursorScrollMargin", 0);
5335 option("cursorHeight", 1, updateSelection, true);
5336 option("singleCursorHeightPerLine", true, updateSelection, true);
5337 option("workTime", 100);
5338 option("workDelay", 100);
5339 option("flattenSpans", true, resetModeState, true);
5340 option("addModeClass", false, resetModeState, true);
5341 option("pollInterval", 100);
5342 option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;});
5343 option("historyEventDelay", 1250);
5344 option("viewportMargin", 10, function(cm){cm.refresh();}, true);
5345 option("maxHighlightLength", 10000, resetModeState, true);
5346 option("moveInputWithCursor", true, function(cm, val) {
5347 if (!val) cm.display.input.resetPosition();
5348 });
5349
5350 option("tabindex", null, function(cm, val) {
5351 cm.display.input.getField().tabIndex = val || "";
5352 });
5353 option("autofocus", null);
5354
5355 // MODE DEFINITION AND QUERYING
5356
5357 // Known modes, by name and by MIME
5358 var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
5359
5360 // Extra arguments are stored as the mode's dependencies, which is
5361 // used by (legacy) mechanisms like loadmode.js to automatically
5362 // load a mode. (Preferred mechanism is the require/define calls.)
5363 CodeMirror.defineMode = function(name, mode) {
5364 if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
5365 if (arguments.length > 2)
5366 mode.dependencies = Array.prototype.slice.call(arguments, 2);
5367 modes[name] = mode;
5368 };
5369
5370 CodeMirror.defineMIME = function(mime, spec) {
5371 mimeModes[mime] = spec;
5372 };
5373
5374 // Given a MIME type, a {name, ...options} config object, or a name
5375 // string, return a mode config object.
5376 CodeMirror.resolveMode = function(spec) {
5377 if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
5378 spec = mimeModes[spec];
5379 } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
5380 var found = mimeModes[spec.name];
5381 if (typeof found == "string") found = {name: found};
5382 spec = createObj(found, spec);
5383 spec.name = found.name;
5384 } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
5385 return CodeMirror.resolveMode("application/xml");
5386 }
5387 if (typeof spec == "string") return {name: spec};
5388 else return spec || {name: "null"};
5389 };
5390
5391 // Given a mode spec (anything that resolveMode accepts), find and
5392 // initialize an actual mode object.
5393 CodeMirror.getMode = function(options, spec) {
5394 var spec = CodeMirror.resolveMode(spec);
5395 var mfactory = modes[spec.name];
5396 if (!mfactory) return CodeMirror.getMode(options, "text/plain");
5397 var modeObj = mfactory(options, spec);
5398 if (modeExtensions.hasOwnProperty(spec.name)) {
5399 var exts = modeExtensions[spec.name];
5400 for (var prop in exts) {
5401 if (!exts.hasOwnProperty(prop)) continue;
5402 if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
5403 modeObj[prop] = exts[prop];
5404 }
5405 }
5406 modeObj.name = spec.name;
5407 if (spec.helperType) modeObj.helperType = spec.helperType;
5408 if (spec.modeProps) for (var prop in spec.modeProps)
5409 modeObj[prop] = spec.modeProps[prop];
5410
5411 return modeObj;
5412 };
5413
5414 // Minimal default mode.
5415 CodeMirror.defineMode("null", function() {
5416 return {token: function(stream) {stream.skipToEnd();}};
5417 });
5418 CodeMirror.defineMIME("text/plain", "null");
5419
5420 // This can be used to attach properties to mode objects from
5421 // outside the actual mode definition.
5422 var modeExtensions = CodeMirror.modeExtensions = {};
5423 CodeMirror.extendMode = function(mode, properties) {
5424 var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
5425 copyObj(properties, exts);
5426 };
5427
5428 // EXTENSIONS
5429
5430 CodeMirror.defineExtension = function(name, func) {
5431 CodeMirror.prototype[name] = func;
5432 };
5433 CodeMirror.defineDocExtension = function(name, func) {
5434 Doc.prototype[name] = func;
5435 };
5436 CodeMirror.defineOption = option;
5437
5438 var initHooks = [];
5439 CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
5440
5441 var helpers = CodeMirror.helpers = {};
5442 CodeMirror.registerHelper = function(type, name, value) {
5443 if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
5444 helpers[type][name] = value;
5445 };
5446 CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
5447 CodeMirror.registerHelper(type, name, value);
5448 helpers[type]._global.push({pred: predicate, val: value});
5449 };
5450
5451 // MODE STATE HANDLING
5452
5453 // Utility functions for working with state. Exported because nested
5454 // modes need to do this for their inner modes.
5455
5456 var copyState = CodeMirror.copyState = function(mode, state) {
5457 if (state === true) return state;
5458 if (mode.copyState) return mode.copyState(state);
5459 var nstate = {};
5460 for (var n in state) {
5461 var val = state[n];
5462 if (val instanceof Array) val = val.concat([]);
5463 nstate[n] = val;
5464 }
5465 return nstate;
5466 };
5467
5468 var startState = CodeMirror.startState = function(mode, a1, a2) {
5469 return mode.startState ? mode.startState(a1, a2) : true;
5470 };
5471
5472 // Given a mode and a state (for that mode), find the inner mode and
5473 // state at the position that the state refers to.
5474 CodeMirror.innerMode = function(mode, state) {
5475 while (mode.innerMode) {
5476 var info = mode.innerMode(state);
5477 if (!info || info.mode == mode) break;
5478 state = info.state;
5479 mode = info.mode;
5480 }
5481 return info || {mode: mode, state: state};
5482 };
5483
5484 // STANDARD COMMANDS
5485
5486 // Commands are parameter-less actions that can be performed on an
5487 // editor, mostly used for keybindings.
5488 var commands = CodeMirror.commands = {
5489 selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);},
5490 singleSelection: function(cm) {
5491 cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll);
5492 },
5493 killLine: function(cm) {
5494 deleteNearSelection(cm, function(range) {
5495 if (range.empty()) {
5496 var len = getLine(cm.doc, range.head.line).text.length;
5497 if (range.head.ch == len && range.head.line < cm.lastLine())
5498 return {from: range.head, to: Pos(range.head.line + 1, 0)};
5499 else
5500 return {from: range.head, to: Pos(range.head.line, len)};
5501 } else {
5502 return {from: range.from(), to: range.to()};
5503 }
5504 });
5505 },
5506 deleteLine: function(cm) {
5507 deleteNearSelection(cm, function(range) {
5508 return {from: Pos(range.from().line, 0),
5509 to: clipPos(cm.doc, Pos(range.to().line + 1, 0))};
5510 });
5511 },
5512 delLineLeft: function(cm) {
5513 deleteNearSelection(cm, function(range) {
5514 return {from: Pos(range.from().line, 0), to: range.from()};
5515 });
5516 },
5517 delWrappedLineLeft: function(cm) {
5518 deleteNearSelection(cm, function(range) {
5519 var top = cm.charCoords(range.head, "div").top + 5;
5520 var leftPos = cm.coordsChar({left: 0, top: top}, "div");
5521 return {from: leftPos, to: range.from()};
5522 });
5523 },
5524 delWrappedLineRight: function(cm) {
5525 deleteNearSelection(cm, function(range) {
5526 var top = cm.charCoords(range.head, "div").top + 5;
5527 var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
5528 return {from: range.from(), to: rightPos };
5529 });
5530 },
5531 undo: function(cm) {cm.undo();},
5532 redo: function(cm) {cm.redo();},
5533 undoSelection: function(cm) {cm.undoSelection();},
5534 redoSelection: function(cm) {cm.redoSelection();},
5535 goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
5536 goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
5537 goLineStart: function(cm) {
5538 cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
5539 {origin: "+move", bias: 1});
5540 },
5541 goLineStartSmart: function(cm) {
5542 cm.extendSelectionsBy(function(range) {
5543 return lineStartSmart(cm, range.head);
5544 }, {origin: "+move", bias: 1});
5545 },
5546 goLineEnd: function(cm) {
5547 cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
5548 {origin: "+move", bias: -1});
5549 },
5550 goLineRight: function(cm) {
5551 cm.extendSelectionsBy(function(range) {
5552 var top = cm.charCoords(range.head, "div").top + 5;
5553 return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
5554 }, sel_move);
5555 },
5556 goLineLeft: function(cm) {
5557 cm.extendSelectionsBy(function(range) {
5558 var top = cm.charCoords(range.head, "div").top + 5;
5559 return cm.coordsChar({left: 0, top: top}, "div");
5560 }, sel_move);
5561 },
5562 goLineLeftSmart: function(cm) {
5563 cm.extendSelectionsBy(function(range) {
5564 var top = cm.charCoords(range.head, "div").top + 5;
5565 var pos = cm.coordsChar({left: 0, top: top}, "div");
5566 if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
5567 return pos;
5568 }, sel_move);
5569 },
5570 goLineUp: function(cm) {cm.moveV(-1, "line");},
5571 goLineDown: function(cm) {cm.moveV(1, "line");},
5572 goPageUp: function(cm) {cm.moveV(-1, "page");},
5573 goPageDown: function(cm) {cm.moveV(1, "page");},
5574 goCharLeft: function(cm) {cm.moveH(-1, "char");},
5575 goCharRight: function(cm) {cm.moveH(1, "char");},
5576 goColumnLeft: function(cm) {cm.moveH(-1, "column");},
5577 goColumnRight: function(cm) {cm.moveH(1, "column");},
5578 goWordLeft: function(cm) {cm.moveH(-1, "word");},
5579 goGroupRight: function(cm) {cm.moveH(1, "group");},
5580 goGroupLeft: function(cm) {cm.moveH(-1, "group");},
5581 goWordRight: function(cm) {cm.moveH(1, "word");},
5582 delCharBefore: function(cm) {cm.deleteH(-1, "char");},
5583 delCharAfter: function(cm) {cm.deleteH(1, "char");},
5584 delWordBefore: function(cm) {cm.deleteH(-1, "word");},
5585 delWordAfter: function(cm) {cm.deleteH(1, "word");},
5586 delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
5587 delGroupAfter: function(cm) {cm.deleteH(1, "group");},
5588 indentAuto: function(cm) {cm.indentSelection("smart");},
5589 indentMore: function(cm) {cm.indentSelection("add");},
5590 indentLess: function(cm) {cm.indentSelection("subtract");},
5591 insertTab: function(cm) {cm.replaceSelection("\t");},
5592 insertSoftTab: function(cm) {
5593 var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize;
5594 for (var i = 0; i < ranges.length; i++) {
5595 var pos = ranges[i].from();
5596 var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize);
5597 spaces.push(new Array(tabSize - col % tabSize + 1).join(" "));
5598 }
5599 cm.replaceSelections(spaces);
5600 },
5601 defaultTab: function(cm) {
5602 if (cm.somethingSelected()) cm.indentSelection("add");
5603 else cm.execCommand("insertTab");
5604 },
5605 transposeChars: function(cm) {
5606 runInOp(cm, function() {
5607 var ranges = cm.listSelections(), newSel = [];
5608 for (var i = 0; i < ranges.length; i++) {
5609 var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
5610 if (line) {
5611 if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
5612 if (cur.ch > 0) {
5613 cur = new Pos(cur.line, cur.ch + 1);
5614 cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
5615 Pos(cur.line, cur.ch - 2), cur, "+transpose");
5616 } else if (cur.line > cm.doc.first) {
5617 var prev = getLine(cm.doc, cur.line - 1).text;
5618 if (prev)
5619 cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1),
5620 Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
5621 }
5622 }
5623 newSel.push(new Range(cur, cur));
5624 }
5625 cm.setSelections(newSel);
5626 });
5627 },
5628 newlineAndIndent: function(cm) {
5629 runInOp(cm, function() {
5630 var len = cm.listSelections().length;
5631 for (var i = 0; i < len; i++) {
5632 var range = cm.listSelections()[i];
5633 cm.replaceRange("\n", range.anchor, range.head, "+input");
5634 cm.indentLine(range.from().line + 1, null, true);
5635 ensureCursorVisible(cm);
5636 }
5637 });
5638 },
5639 toggleOverwrite: function(cm) {cm.toggleOverwrite();}
5640 };
5641
5642
5643 // STANDARD KEYMAPS
5644
5645 var keyMap = CodeMirror.keyMap = {};
5646
5647 keyMap.basic = {
5648 "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
5649 "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
5650 "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
5651 "Tab": "defaultTab", "Shift-Tab": "indentAuto",
5652 "Enter": "newlineAndIndent", "Insert": "toggleOverwrite",
5653 "Esc": "singleSelection"
5654 };
5655 // Note that the save and find-related commands aren't defined by
5656 // default. User code or addons can define them. Unknown commands
5657 // are simply ignored.
5658 keyMap.pcDefault = {
5659 "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
5660 "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown",
5661 "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
5662 "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
5663 "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
5664 "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
5665 "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
5666 fallthrough: "basic"
5667 };
5668 // Very basic readline/emacs-style bindings, which are standard on Mac.
5669 keyMap.emacsy = {
5670 "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
5671 "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
5672 "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
5673 "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
5674 };
5675 keyMap.macDefault = {
5676 "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
5677 "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
5678 "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
5679 "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
5680 "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
5681 "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
5682 "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
5683 fallthrough: ["basic", "emacsy"]
5684 };
5685 keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
5686
5687 // KEYMAP DISPATCH
5688
5689 function normalizeKeyName(name) {
5690 var parts = name.split(/-(?!$)/), name = parts[parts.length - 1];
5691 var alt, ctrl, shift, cmd;
5692 for (var i = 0; i < parts.length - 1; i++) {
5693 var mod = parts[i];
5694 if (/^(cmd|meta|m)$/i.test(mod)) cmd = true;
5695 else if (/^a(lt)?$/i.test(mod)) alt = true;
5696 else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
5697 else if (/^s(hift)$/i.test(mod)) shift = true;
5698 else throw new Error("Unrecognized modifier name: " + mod);
5699 }
5700 if (alt) name = "Alt-" + name;
5701 if (ctrl) name = "Ctrl-" + name;
5702 if (cmd) name = "Cmd-" + name;
5703 if (shift) name = "Shift-" + name;
5704 return name;
5705 }
5706
5707 // This is a kludge to keep keymaps mostly working as raw objects
5708 // (backwards compatibility) while at the same time support features
5709 // like normalization and multi-stroke key bindings. It compiles a
5710 // new normalized keymap, and then updates the old object to reflect
5711 // this.
5712 CodeMirror.normalizeKeyMap = function(keymap) {
5713 var copy = {};
5714 for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) {
5715 var value = keymap[keyname];
5716 if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue;
5717 if (value == "...") { delete keymap[keyname]; continue; }
5718
5719 var keys = map(keyname.split(" "), normalizeKeyName);
5720 for (var i = 0; i < keys.length; i++) {
5721 var val, name;
5722 if (i == keys.length - 1) {
5723 name = keyname;
5724 val = value;
5725 } else {
5726 name = keys.slice(0, i + 1).join(" ");
5727 val = "...";
5728 }
5729 var prev = copy[name];
5730 if (!prev) copy[name] = val;
5731 else if (prev != val) throw new Error("Inconsistent bindings for " + name);
5732 }
5733 delete keymap[keyname];
5734 }
5735 for (var prop in copy) keymap[prop] = copy[prop];
5736 return keymap;
5737 };
5738
5739 var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) {
5740 map = getKeyMap(map);
5741 var found = map.call ? map.call(key, context) : map[key];
5742 if (found === false) return "nothing";
5743 if (found === "...") return "multi";
5744 if (found != null && handle(found)) return "handled";
5745
5746 if (map.fallthrough) {
5747 if (Object.prototype.toString.call(map.fallthrough) != "[object Array]")
5748 return lookupKey(key, map.fallthrough, handle, context);
5749 for (var i = 0; i < map.fallthrough.length; i++) {
5750 var result = lookupKey(key, map.fallthrough[i], handle, context);
5751 if (result) return result;
5752 }
5753 }
5754 };
5755
5756 // Modifier key presses don't count as 'real' key presses for the
5757 // purpose of keymap fallthrough.
5758 var isModifierKey = CodeMirror.isModifierKey = function(value) {
5759 var name = typeof value == "string" ? value : keyNames[value.keyCode];
5760 return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
5761 };
5762
5763 // Look up the name of a key as indicated by an event object.
5764 var keyName = CodeMirror.keyName = function(event, noShift) {
5765 if (presto && event.keyCode == 34 && event["char"]) return false;
5766 var base = keyNames[event.keyCode], name = base;
5767 if (name == null || event.altGraphKey) return false;
5768 if (event.altKey && base != "Alt") name = "Alt-" + name;
5769 if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name;
5770 if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name;
5771 if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name;
5772 return name;
5773 };
5774
5775 function getKeyMap(val) {
5776 return typeof val == "string" ? keyMap[val] : val;
5777 }
5778
5779 // FROMTEXTAREA
5780
5781 CodeMirror.fromTextArea = function(textarea, options) {
5782 options = options ? copyObj(options) : {};
5783 options.value = textarea.value;
5784 if (!options.tabindex && textarea.tabIndex)
5785 options.tabindex = textarea.tabIndex;
5786 if (!options.placeholder && textarea.placeholder)
5787 options.placeholder = textarea.placeholder;
5788 // Set autofocus to true if this textarea is focused, or if it has
5789 // autofocus and no other element is focused.
5790 if (options.autofocus == null) {
5791 var hasFocus = activeElt();
5792 options.autofocus = hasFocus == textarea ||
5793 textarea.getAttribute("autofocus") != null && hasFocus == document.body;
5794 }
5795
5796 function save() {textarea.value = cm.getValue();}
5797 if (textarea.form) {
5798 on(textarea.form, "submit", save);
5799 // Deplorable hack to make the submit method do the right thing.
5800 if (!options.leaveSubmitMethodAlone) {
5801 var form = textarea.form, realSubmit = form.submit;
5802 try {
5803 var wrappedSubmit = form.submit = function() {
5804 save();
5805 form.submit = realSubmit;
5806 form.submit();
5807 form.submit = wrappedSubmit;
5808 };
5809 } catch(e) {}
5810 }
5811 }
5812
5813 options.finishInit = function(cm) {
5814 cm.save = save;
5815 cm.getTextArea = function() { return textarea; };
5816 cm.toTextArea = function() {
5817 cm.toTextArea = isNaN; // Prevent this from being ran twice
5818 save();
5819 textarea.parentNode.removeChild(cm.getWrapperElement());
5820 textarea.style.display = "";
5821 if (textarea.form) {
5822 off(textarea.form, "submit", save);
5823 if (typeof textarea.form.submit == "function")
5824 textarea.form.submit = realSubmit;
5825 }
5826 };
5827 };
5828
5829 textarea.style.display = "none";
5830 var cm = CodeMirror(function(node) {
5831 textarea.parentNode.insertBefore(node, textarea.nextSibling);
5832 }, options);
5833 return cm;
5834 };
5835
5836 // STRING STREAM
5837
5838 // Fed to the mode parsers, provides helper functions to make
5839 // parsers more succinct.
5840
5841 var StringStream = CodeMirror.StringStream = function(string, tabSize) {
5842 this.pos = this.start = 0;
5843 this.string = string;
5844 this.tabSize = tabSize || 8;
5845 this.lastColumnPos = this.lastColumnValue = 0;
5846 this.lineStart = 0;
5847 };
5848
5849 StringStream.prototype = {
5850 eol: function() {return this.pos >= this.string.length;},
5851 sol: function() {return this.pos == this.lineStart;},
5852 peek: function() {return this.string.charAt(this.pos) || undefined;},
5853 next: function() {
5854 if (this.pos < this.string.length)
5855 return this.string.charAt(this.pos++);
5856 },
5857 eat: function(match) {
5858 var ch = this.string.charAt(this.pos);
5859 if (typeof match == "string") var ok = ch == match;
5860 else var ok = ch && (match.test ? match.test(ch) : match(ch));
5861 if (ok) {++this.pos; return ch;}
5862 },
5863 eatWhile: function(match) {
5864 var start = this.pos;
5865 while (this.eat(match)){}
5866 return this.pos > start;
5867 },
5868 eatSpace: function() {
5869 var start = this.pos;
5870 while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
5871 return this.pos > start;
5872 },
5873 skipToEnd: function() {this.pos = this.string.length;},
5874 skipTo: function(ch) {
5875 var found = this.string.indexOf(ch, this.pos);
5876 if (found > -1) {this.pos = found; return true;}
5877 },
5878 backUp: function(n) {this.pos -= n;},
5879 column: function() {
5880 if (this.lastColumnPos < this.start) {
5881 this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
5882 this.lastColumnPos = this.start;
5883 }
5884 return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
5885 },
5886 indentation: function() {
5887 return countColumn(this.string, null, this.tabSize) -
5888 (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
5889 },
5890 match: function(pattern, consume, caseInsensitive) {
5891 if (typeof pattern == "string") {
5892 var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
5893 var substr = this.string.substr(this.pos, pattern.length);
5894 if (cased(substr) == cased(pattern)) {
5895 if (consume !== false) this.pos += pattern.length;
5896 return true;
5897 }
5898 } else {
5899 var match = this.string.slice(this.pos).match(pattern);
5900 if (match && match.index > 0) return null;
5901 if (match && consume !== false) this.pos += match[0].length;
5902 return match;
5903 }
5904 },
5905 current: function(){return this.string.slice(this.start, this.pos);},
5906 hideFirstChars: function(n, inner) {
5907 this.lineStart += n;
5908 try { return inner(); }
5909 finally { this.lineStart -= n; }
5910 }
5911 };
5912
5913 // TEXTMARKERS
5914
5915 // Created with markText and setBookmark methods. A TextMarker is a
5916 // handle that can be used to clear or find a marked position in the
5917 // document. Line objects hold arrays (markedSpans) containing
5918 // {from, to, marker} object pointing to such marker objects, and
5919 // indicating that such a marker is present on that line. Multiple
5920 // lines may point to the same marker when it spans across lines.
5921 // The spans will have null for their from/to properties when the
5922 // marker continues beyond the start/end of the line. Markers have
5923 // links back to the lines they currently touch.
5924
5925 var nextMarkerId = 0;
5926
5927 var TextMarker = CodeMirror.TextMarker = function(doc, type) {
5928 this.lines = [];
5929 this.type = type;
5930 this.doc = doc;
5931 this.id = ++nextMarkerId;
5932 };
5933 eventMixin(TextMarker);
5934
5935 // Clear the marker.
5936 TextMarker.prototype.clear = function() {
5937 if (this.explicitlyCleared) return;
5938 var cm = this.doc.cm, withOp = cm && !cm.curOp;
5939 if (withOp) startOperation(cm);
5940 if (hasHandler(this, "clear")) {
5941 var found = this.find();
5942 if (found) signalLater(this, "clear", found.from, found.to);
5943 }
5944 var min = null, max = null;
5945 for (var i = 0; i < this.lines.length; ++i) {
5946 var line = this.lines[i];
5947 var span = getMarkedSpanFor(line.markedSpans, this);
5948 if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text");
5949 else if (cm) {
5950 if (span.to != null) max = lineNo(line);
5951 if (span.from != null) min = lineNo(line);
5952 }
5953 line.markedSpans = removeMarkedSpan(line.markedSpans, span);
5954 if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm)
5955 updateLineHeight(line, textHeight(cm.display));
5956 }
5957 if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
5958 var visual = visualLine(this.lines[i]), len = lineLength(visual);
5959 if (len > cm.display.maxLineLength) {
5960 cm.display.maxLine = visual;
5961 cm.display.maxLineLength = len;
5962 cm.display.maxLineChanged = true;
5963 }
5964 }
5965
5966 if (min != null && cm && this.collapsed) regChange(cm, min, max + 1);
5967 this.lines.length = 0;
5968 this.explicitlyCleared = true;
5969 if (this.atomic && this.doc.cantEdit) {
5970 this.doc.cantEdit = false;
5971 if (cm) reCheckSelection(cm.doc);
5972 }
5973 if (cm) signalLater(cm, "markerCleared", cm, this);
5974 if (withOp) endOperation(cm);
5975 if (this.parent) this.parent.clear();
5976 };
5977
5978 // Find the position of the marker in the document. Returns a {from,
5979 // to} object by default. Side can be passed to get a specific side
5980 // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the
5981 // Pos objects returned contain a line object, rather than a line
5982 // number (used to prevent looking up the same line twice).
5983 TextMarker.prototype.find = function(side, lineObj) {
5984 if (side == null && this.type == "bookmark") side = 1;
5985 var from, to;
5986 for (var i = 0; i < this.lines.length; ++i) {
5987 var line = this.lines[i];
5988 var span = getMarkedSpanFor(line.markedSpans, this);
5989 if (span.from != null) {
5990 from = Pos(lineObj ? line : lineNo(line), span.from);
5991 if (side == -1) return from;
5992 }
5993 if (span.to != null) {
5994 to = Pos(lineObj ? line : lineNo(line), span.to);
5995 if (side == 1) return to;
5996 }
5997 }
5998 return from && {from: from, to: to};
5999 };
6000
6001 // Signals that the marker's widget changed, and surrounding layout
6002 // should be recomputed.
6003 TextMarker.prototype.changed = function() {
6004 var pos = this.find(-1, true), widget = this, cm = this.doc.cm;
6005 if (!pos || !cm) return;
6006 runInOp(cm, function() {
6007 var line = pos.line, lineN = lineNo(pos.line);
6008 var view = findViewForLine(cm, lineN);
6009 if (view) {
6010 clearLineMeasurementCacheFor(view);
6011 cm.curOp.selectionChanged = cm.curOp.forceUpdate = true;
6012 }
6013 cm.curOp.updateMaxLine = true;
6014 if (!lineIsHidden(widget.doc, line) && widget.height != null) {
6015 var oldHeight = widget.height;
6016 widget.height = null;
6017 var dHeight = widgetHeight(widget) - oldHeight;
6018 if (dHeight)
6019 updateLineHeight(line, line.height + dHeight);
6020 }
6021 });
6022 };
6023
6024 TextMarker.prototype.attachLine = function(line) {
6025 if (!this.lines.length && this.doc.cm) {
6026 var op = this.doc.cm.curOp;
6027 if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
6028 (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
6029 }
6030 this.lines.push(line);
6031 };
6032 TextMarker.prototype.detachLine = function(line) {
6033 this.lines.splice(indexOf(this.lines, line), 1);
6034 if (!this.lines.length && this.doc.cm) {
6035 var op = this.doc.cm.curOp;
6036 (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
6037 }
6038 };
6039
6040 // Collapsed markers have unique ids, in order to be able to order
6041 // them, which is needed for uniquely determining an outer marker
6042 // when they overlap (they may nest, but not partially overlap).
6043 var nextMarkerId = 0;
6044
6045 // Create a marker, wire it up to the right lines, and
6046 function markText(doc, from, to, options, type) {
6047 // Shared markers (across linked documents) are handled separately
6048 // (markTextShared will call out to this again, once per
6049 // document).
6050 if (options && options.shared) return markTextShared(doc, from, to, options, type);
6051 // Ensure we are in an operation.
6052 if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
6053
6054 var marker = new TextMarker(doc, type), diff = cmp(from, to);
6055 if (options) copyObj(options, marker, false);
6056 // Don't connect empty markers unless clearWhenEmpty is false
6057 if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)
6058 return marker;
6059 if (marker.replacedWith) {
6060 // Showing up as a widget implies collapsed (widget replaces text)
6061 marker.collapsed = true;
6062 marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget");
6063 if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true");
6064 if (options.insertLeft) marker.widgetNode.insertLeft = true;
6065 }
6066 if (marker.collapsed) {
6067 if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
6068 from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
6069 throw new Error("Inserting collapsed marker partially overlapping an existing one");
6070 sawCollapsedSpans = true;
6071 }
6072
6073 if (marker.addToHistory)
6074 addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN);
6075
6076 var curLine = from.line, cm = doc.cm, updateMaxLine;
6077 doc.iter(curLine, to.line + 1, function(line) {
6078 if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)
6079 updateMaxLine = true;
6080 if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
6081 addMarkedSpan(line, new MarkedSpan(marker,
6082 curLine == from.line ? from.ch : null,
6083 curLine == to.line ? to.ch : null));
6084 ++curLine;
6085 });
6086 // lineIsHidden depends on the presence of the spans, so needs a second pass
6087 if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
6088 if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
6089 });
6090
6091 if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
6092
6093 if (marker.readOnly) {
6094 sawReadOnlySpans = true;
6095 if (doc.history.done.length || doc.history.undone.length)
6096 doc.clearHistory();
6097 }
6098 if (marker.collapsed) {
6099 marker.id = ++nextMarkerId;
6100 marker.atomic = true;
6101 }
6102 if (cm) {
6103 // Sync editor state
6104 if (updateMaxLine) cm.curOp.updateMaxLine = true;
6105 if (marker.collapsed)
6106 regChange(cm, from.line, to.line + 1);
6107 else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)
6108 for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text");
6109 if (marker.atomic) reCheckSelection(cm.doc);
6110 signalLater(cm, "markerAdded", cm, marker);
6111 }
6112 return marker;
6113 }
6114
6115 // SHARED TEXTMARKERS
6116
6117 // A shared marker spans multiple linked documents. It is
6118 // implemented as a meta-marker-object controlling multiple normal
6119 // markers.
6120 var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) {
6121 this.markers = markers;
6122 this.primary = primary;
6123 for (var i = 0; i < markers.length; ++i)
6124 markers[i].parent = this;
6125 };
6126 eventMixin(SharedTextMarker);
6127
6128 SharedTextMarker.prototype.clear = function() {
6129 if (this.explicitlyCleared) return;
6130 this.explicitlyCleared = true;
6131 for (var i = 0; i < this.markers.length; ++i)
6132 this.markers[i].clear();
6133 signalLater(this, "clear");
6134 };
6135 SharedTextMarker.prototype.find = function(side, lineObj) {
6136 return this.primary.find(side, lineObj);
6137 };
6138
6139 function markTextShared(doc, from, to, options, type) {
6140 options = copyObj(options);
6141 options.shared = false;
6142 var markers = [markText(doc, from, to, options, type)], primary = markers[0];
6143 var widget = options.widgetNode;
6144 linkedDocs(doc, function(doc) {
6145 if (widget) options.widgetNode = widget.cloneNode(true);
6146 markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
6147 for (var i = 0; i < doc.linked.length; ++i)
6148 if (doc.linked[i].isParent) return;
6149 primary = lst(markers);
6150 });
6151 return new SharedTextMarker(markers, primary);
6152 }
6153
6154 function findSharedMarkers(doc) {
6155 return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())),
6156 function(m) { return m.parent; });
6157 }
6158
6159 function copySharedMarkers(doc, markers) {
6160 for (var i = 0; i < markers.length; i++) {
6161 var marker = markers[i], pos = marker.find();
6162 var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to);
6163 if (cmp(mFrom, mTo)) {
6164 var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type);
6165 marker.markers.push(subMark);
6166 subMark.parent = marker;
6167 }
6168 }
6169 }
6170
6171 function detachSharedMarkers(markers) {
6172 for (var i = 0; i < markers.length; i++) {
6173 var marker = markers[i], linked = [marker.primary.doc];;
6174 linkedDocs(marker.primary.doc, function(d) { linked.push(d); });
6175 for (var j = 0; j < marker.markers.length; j++) {
6176 var subMarker = marker.markers[j];
6177 if (indexOf(linked, subMarker.doc) == -1) {
6178 subMarker.parent = null;
6179 marker.markers.splice(j--, 1);
6180 }
6181 }
6182 }
6183 }
6184
6185 // TEXTMARKER SPANS
6186
6187 function MarkedSpan(marker, from, to) {
6188 this.marker = marker;
6189 this.from = from; this.to = to;
6190 }
6191
6192 // Search an array of spans for a span matching the given marker.
6193 function getMarkedSpanFor(spans, marker) {
6194 if (spans) for (var i = 0; i < spans.length; ++i) {
6195 var span = spans[i];
6196 if (span.marker == marker) return span;
6197 }
6198 }
6199 // Remove a span from an array, returning undefined if no spans are
6200 // left (we don't store arrays for lines without spans).
6201 function removeMarkedSpan(spans, span) {
6202 for (var r, i = 0; i < spans.length; ++i)
6203 if (spans[i] != span) (r || (r = [])).push(spans[i]);
6204 return r;
6205 }
6206 // Add a span to a line.
6207 function addMarkedSpan(line, span) {
6208 line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
6209 span.marker.attachLine(line);
6210 }
6211
6212 // Used for the algorithm that adjusts markers for a change in the
6213 // document. These functions cut an array of spans at a given
6214 // character position, returning an array of remaining chunks (or
6215 // undefined if nothing remains).
6216 function markedSpansBefore(old, startCh, isInsert) {
6217 if (old) for (var i = 0, nw; i < old.length; ++i) {
6218 var span = old[i], marker = span.marker;
6219 var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
6220 if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
6221 var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
6222 (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to));
6223 }
6224 }
6225 return nw;
6226 }
6227 function markedSpansAfter(old, endCh, isInsert) {
6228 if (old) for (var i = 0, nw; i < old.length; ++i) {
6229 var span = old[i], marker = span.marker;
6230 var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
6231 if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
6232 var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
6233 (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
6234 span.to == null ? null : span.to - endCh));
6235 }
6236 }
6237 return nw;
6238 }
6239
6240 // Given a change object, compute the new set of marker spans that
6241 // cover the line in which the change took place. Removes spans
6242 // entirely within the change, reconnects spans belonging to the
6243 // same marker that appear on both sides of the change, and cuts off
6244 // spans partially within the change. Returns an array of span
6245 // arrays with one element for each line in (after) the change.
6246 function stretchSpansOverChange(doc, change) {
6247 if (change.full) return null;
6248 var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
6249 var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
6250 if (!oldFirst && !oldLast) return null;
6251
6252 var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0;
6253 // Get the spans that 'stick out' on both sides
6254 var first = markedSpansBefore(oldFirst, startCh, isInsert);
6255 var last = markedSpansAfter(oldLast, endCh, isInsert);
6256
6257 // Next, merge those two ends
6258 var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
6259 if (first) {
6260 // Fix up .to properties of first
6261 for (var i = 0; i < first.length; ++i) {
6262 var span = first[i];
6263 if (span.to == null) {
6264 var found = getMarkedSpanFor(last, span.marker);
6265 if (!found) span.to = startCh;
6266 else if (sameLine) span.to = found.to == null ? null : found.to + offset;
6267 }
6268 }
6269 }
6270 if (last) {
6271 // Fix up .from in last (or move them into first in case of sameLine)
6272 for (var i = 0; i < last.length; ++i) {
6273 var span = last[i];
6274 if (span.to != null) span.to += offset;
6275 if (span.from == null) {
6276 var found = getMarkedSpanFor(first, span.marker);
6277 if (!found) {
6278 span.from = offset;
6279 if (sameLine) (first || (first = [])).push(span);
6280 }
6281 } else {
6282 span.from += offset;
6283 if (sameLine) (first || (first = [])).push(span);
6284 }
6285 }
6286 }
6287 // Make sure we didn't create any zero-length spans
6288 if (first) first = clearEmptySpans(first);
6289 if (last && last != first) last = clearEmptySpans(last);
6290
6291 var newMarkers = [first];
6292 if (!sameLine) {
6293 // Fill gap with whole-line-spans
6294 var gap = change.text.length - 2, gapMarkers;
6295 if (gap > 0 && first)
6296 for (var i = 0; i < first.length; ++i)
6297 if (first[i].to == null)
6298 (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null));
6299 for (var i = 0; i < gap; ++i)
6300 newMarkers.push(gapMarkers);
6301 newMarkers.push(last);
6302 }
6303 return newMarkers;
6304 }
6305
6306 // Remove spans that are empty and don't have a clearWhenEmpty
6307 // option of false.
6308 function clearEmptySpans(spans) {
6309 for (var i = 0; i < spans.length; ++i) {
6310 var span = spans[i];
6311 if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
6312 spans.splice(i--, 1);
6313 }
6314 if (!spans.length) return null;
6315 return spans;
6316 }
6317
6318 // Used for un/re-doing changes from the history. Combines the
6319 // result of computing the existing spans with the set of spans that
6320 // existed in the history (so that deleting around a span and then
6321 // undoing brings back the span).
6322 function mergeOldSpans(doc, change) {
6323 var old = getOldSpans(doc, change);
6324 var stretched = stretchSpansOverChange(doc, change);
6325 if (!old) return stretched;
6326 if (!stretched) return old;
6327
6328 for (var i = 0; i < old.length; ++i) {
6329 var oldCur = old[i], stretchCur = stretched[i];
6330 if (oldCur && stretchCur) {
6331 spans: for (var j = 0; j < stretchCur.length; ++j) {
6332 var span = stretchCur[j];
6333 for (var k = 0; k < oldCur.length; ++k)
6334 if (oldCur[k].marker == span.marker) continue spans;
6335 oldCur.push(span);
6336 }
6337 } else if (stretchCur) {
6338 old[i] = stretchCur;
6339 }
6340 }
6341 return old;
6342 }
6343
6344 // Used to 'clip' out readOnly ranges when making a change.
6345 function removeReadOnlyRanges(doc, from, to) {
6346 var markers = null;
6347 doc.iter(from.line, to.line + 1, function(line) {
6348 if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
6349 var mark = line.markedSpans[i].marker;
6350 if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
6351 (markers || (markers = [])).push(mark);
6352 }
6353 });
6354 if (!markers) return null;
6355 var parts = [{from: from, to: to}];
6356 for (var i = 0; i < markers.length; ++i) {
6357 var mk = markers[i], m = mk.find(0);
6358 for (var j = 0; j < parts.length; ++j) {
6359 var p = parts[j];
6360 if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue;
6361 var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to);
6362 if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
6363 newParts.push({from: p.from, to: m.from});
6364 if (dto > 0 || !mk.inclusiveRight && !dto)
6365 newParts.push({from: m.to, to: p.to});
6366 parts.splice.apply(parts, newParts);
6367 j += newParts.length - 1;
6368 }
6369 }
6370 return parts;
6371 }
6372
6373 // Connect or disconnect spans from a line.
6374 function detachMarkedSpans(line) {
6375 var spans = line.markedSpans;
6376 if (!spans) return;
6377 for (var i = 0; i < spans.length; ++i)
6378 spans[i].marker.detachLine(line);
6379 line.markedSpans = null;
6380 }
6381 function attachMarkedSpans(line, spans) {
6382 if (!spans) return;
6383 for (var i = 0; i < spans.length; ++i)
6384 spans[i].marker.attachLine(line);
6385 line.markedSpans = spans;
6386 }
6387
6388 // Helpers used when computing which overlapping collapsed span
6389 // counts as the larger one.
6390 function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
6391 function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
6392
6393 // Returns a number indicating which of two overlapping collapsed
6394 // spans is larger (and thus includes the other). Falls back to
6395 // comparing ids when the spans cover exactly the same range.
6396 function compareCollapsedMarkers(a, b) {
6397 var lenDiff = a.lines.length - b.lines.length;
6398 if (lenDiff != 0) return lenDiff;
6399 var aPos = a.find(), bPos = b.find();
6400 var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
6401 if (fromCmp) return -fromCmp;
6402 var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
6403 if (toCmp) return toCmp;
6404 return b.id - a.id;
6405 }
6406
6407 // Find out whether a line ends or starts in a collapsed span. If
6408 // so, return the marker for that span.
6409 function collapsedSpanAtSide(line, start) {
6410 var sps = sawCollapsedSpans && line.markedSpans, found;
6411 if (sps) for (var sp, i = 0; i < sps.length; ++i) {
6412 sp = sps[i];
6413 if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
6414 (!found || compareCollapsedMarkers(found, sp.marker) < 0))
6415 found = sp.marker;
6416 }
6417 return found;
6418 }
6419 function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
6420 function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
6421
6422 // Test whether there exists a collapsed span that partially
6423 // overlaps (covers the start or end, but not both) of a new span.
6424 // Such overlap is not allowed.
6425 function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
6426 var line = getLine(doc, lineNo);
6427 var sps = sawCollapsedSpans && line.markedSpans;
6428 if (sps) for (var i = 0; i < sps.length; ++i) {
6429 var sp = sps[i];
6430 if (!sp.marker.collapsed) continue;
6431 var found = sp.marker.find(0);
6432 var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
6433 var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
6434 if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
6435 if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) ||
6436 fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight)))
6437 return true;
6438 }
6439 }
6440
6441 // A visual line is a line as drawn on the screen. Folding, for
6442 // example, can cause multiple logical lines to appear on the same
6443 // visual line. This finds the start of the visual line that the
6444 // given line is part of (usually that is the line itself).
6445 function visualLine(line) {
6446 var merged;
6447 while (merged = collapsedSpanAtStart(line))
6448 line = merged.find(-1, true).line;
6449 return line;
6450 }
6451
6452 // Returns an array of logical lines that continue the visual line
6453 // started by the argument, or undefined if there are no such lines.
6454 function visualLineContinued(line) {
6455 var merged, lines;
6456 while (merged = collapsedSpanAtEnd(line)) {
6457 line = merged.find(1, true).line;
6458 (lines || (lines = [])).push(line);
6459 }
6460 return lines;
6461 }
6462
6463 // Get the line number of the start of the visual line that the
6464 // given line number is part of.
6465 function visualLineNo(doc, lineN) {
6466 var line = getLine(doc, lineN), vis = visualLine(line);
6467 if (line == vis) return lineN;
6468 return lineNo(vis);
6469 }
6470 // Get the line number of the start of the next visual line after
6471 // the given line.
6472 function visualLineEndNo(doc, lineN) {
6473 if (lineN > doc.lastLine()) return lineN;
6474 var line = getLine(doc, lineN), merged;
6475 if (!lineIsHidden(doc, line)) return lineN;
6476 while (merged = collapsedSpanAtEnd(line))
6477 line = merged.find(1, true).line;
6478 return lineNo(line) + 1;
6479 }
6480
6481 // Compute whether a line is hidden. Lines count as hidden when they
6482 // are part of a visual line that starts with another line, or when
6483 // they are entirely covered by collapsed, non-widget span.
6484 function lineIsHidden(doc, line) {
6485 var sps = sawCollapsedSpans && line.markedSpans;
6486 if (sps) for (var sp, i = 0; i < sps.length; ++i) {
6487 sp = sps[i];
6488 if (!sp.marker.collapsed) continue;
6489 if (sp.from == null) return true;
6490 if (sp.marker.widgetNode) continue;
6491 if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
6492 return true;
6493 }
6494 }
6495 function lineIsHiddenInner(doc, line, span) {
6496 if (span.to == null) {
6497 var end = span.marker.find(1, true);
6498 return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker));
6499 }
6500 if (span.marker.inclusiveRight && span.to == line.text.length)
6501 return true;
6502 for (var sp, i = 0; i < line.markedSpans.length; ++i) {
6503 sp = line.markedSpans[i];
6504 if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
6505 (sp.to == null || sp.to != span.from) &&
6506 (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
6507 lineIsHiddenInner(doc, line, sp)) return true;
6508 }
6509 }
6510
6511 // LINE WIDGETS
6512
6513 // Line widgets are block elements displayed above or below a line.
6514
6515 var LineWidget = CodeMirror.LineWidget = function(doc, node, options) {
6516 if (options) for (var opt in options) if (options.hasOwnProperty(opt))
6517 this[opt] = options[opt];
6518 this.doc = doc;
6519 this.node = node;
6520 };
6521 eventMixin(LineWidget);
6522
6523 function adjustScrollWhenAboveVisible(cm, line, diff) {
6524 if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))
6525 addToScrollPos(cm, null, diff);
6526 }
6527
6528 LineWidget.prototype.clear = function() {
6529 var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line);
6530 if (no == null || !ws) return;
6531 for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
6532 if (!ws.length) line.widgets = null;
6533 var height = widgetHeight(this);
6534 updateLineHeight(line, Math.max(0, line.height - height));
6535 if (cm) runInOp(cm, function() {
6536 adjustScrollWhenAboveVisible(cm, line, -height);
6537 regLineChange(cm, no, "widget");
6538 });
6539 };
6540 LineWidget.prototype.changed = function() {
6541 var oldH = this.height, cm = this.doc.cm, line = this.line;
6542 this.height = null;
6543 var diff = widgetHeight(this) - oldH;
6544 if (!diff) return;
6545 updateLineHeight(line, line.height + diff);
6546 if (cm) runInOp(cm, function() {
6547 cm.curOp.forceUpdate = true;
6548 adjustScrollWhenAboveVisible(cm, line, diff);
6549 });
6550 };
6551
6552 function widgetHeight(widget) {
6553 if (widget.height != null) return widget.height;
6554 var cm = widget.doc.cm;
6555 if (!cm) return 0;
6556 if (!contains(document.body, widget.node)) {
6557 var parentStyle = "position: relative;";
6558 if (widget.coverGutter)
6559 parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;";
6560 if (widget.noHScroll)
6561 parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;";
6562 removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle));
6563 }
6564 return widget.height = widget.node.offsetHeight;
6565 }
6566
6567 function addLineWidget(doc, handle, node, options) {
6568 var widget = new LineWidget(doc, node, options);
6569 var cm = doc.cm;
6570 if (cm && widget.noHScroll) cm.display.alignWidgets = true;
6571 changeLine(doc, handle, "widget", function(line) {
6572 var widgets = line.widgets || (line.widgets = []);
6573 if (widget.insertAt == null) widgets.push(widget);
6574 else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
6575 widget.line = line;
6576 if (cm && !lineIsHidden(doc, line)) {
6577 var aboveVisible = heightAtLine(line) < doc.scrollTop;
6578 updateLineHeight(line, line.height + widgetHeight(widget));
6579 if (aboveVisible) addToScrollPos(cm, null, widget.height);
6580 cm.curOp.forceUpdate = true;
6581 }
6582 return true;
6583 });
6584 return widget;
6585 }
6586
6587 // LINE DATA STRUCTURE
6588
6589 // Line objects. These hold state related to a line, including
6590 // highlighting info (the styles array).
6591 var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
6592 this.text = text;
6593 attachMarkedSpans(this, markedSpans);
6594 this.height = estimateHeight ? estimateHeight(this) : 1;
6595 };
6596 eventMixin(Line);
6597 Line.prototype.lineNo = function() { return lineNo(this); };
6598
6599 // Change the content (text, markers) of a line. Automatically
6600 // invalidates cached information and tries to re-estimate the
6601 // line's height.
6602 function updateLine(line, text, markedSpans, estimateHeight) {
6603 line.text = text;
6604 if (line.stateAfter) line.stateAfter = null;
6605 if (line.styles) line.styles = null;
6606 if (line.order != null) line.order = null;
6607 detachMarkedSpans(line);
6608 attachMarkedSpans(line, markedSpans);
6609 var estHeight = estimateHeight ? estimateHeight(line) : 1;
6610 if (estHeight != line.height) updateLineHeight(line, estHeight);
6611 }
6612
6613 // Detach a line from the document tree and its markers.
6614 function cleanUpLine(line) {
6615 line.parent = null;
6616 detachMarkedSpans(line);
6617 }
6618
6619 function extractLineClasses(type, output) {
6620 if (type) for (;;) {
6621 var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/);
6622 if (!lineClass) break;
6623 type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length);
6624 var prop = lineClass[1] ? "bgClass" : "textClass";
6625 if (output[prop] == null)
6626 output[prop] = lineClass[2];
6627 else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
6628 output[prop] += " " + lineClass[2];
6629 }
6630 return type;
6631 }
6632
6633 function callBlankLine(mode, state) {
6634 if (mode.blankLine) return mode.blankLine(state);
6635 if (!mode.innerMode) return;
6636 var inner = CodeMirror.innerMode(mode, state);
6637 if (inner.mode.blankLine) return inner.mode.blankLine(inner.state);
6638 }
6639
6640 function readToken(mode, stream, state, inner) {
6641 for (var i = 0; i < 10; i++) {
6642 if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode;
6643 var style = mode.token(stream, state);
6644 if (stream.pos > stream.start) return style;
6645 }
6646 throw new Error("Mode " + mode.name + " failed to advance stream.");
6647 }
6648
6649 // Utility for getTokenAt and getLineTokens
6650 function takeToken(cm, pos, precise, asArray) {
6651 function getObj(copy) {
6652 return {start: stream.start, end: stream.pos,
6653 string: stream.current(),
6654 type: style || null,
6655 state: copy ? copyState(doc.mode, state) : state};
6656 }
6657
6658 var doc = cm.doc, mode = doc.mode, style;
6659 pos = clipPos(doc, pos);
6660 var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise);
6661 var stream = new StringStream(line.text, cm.options.tabSize), tokens;
6662 if (asArray) tokens = [];
6663 while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
6664 stream.start = stream.pos;
6665 style = readToken(mode, stream, state);
6666 if (asArray) tokens.push(getObj(true));
6667 }
6668 return asArray ? tokens : getObj();
6669 }
6670
6671 // Run the given mode's parser over a line, calling f for each token.
6672 function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {
6673 var flattenSpans = mode.flattenSpans;
6674 if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
6675 var curStart = 0, curStyle = null;
6676 var stream = new StringStream(text, cm.options.tabSize), style;
6677 var inner = cm.options.addModeClass && [null];
6678 if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses);
6679 while (!stream.eol()) {
6680 if (stream.pos > cm.options.maxHighlightLength) {
6681 flattenSpans = false;
6682 if (forceToEnd) processLine(cm, text, state, stream.pos);
6683 stream.pos = text.length;
6684 style = null;
6685 } else {
6686 style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses);
6687 }
6688 if (inner) {
6689 var mName = inner[0].name;
6690 if (mName) style = "m-" + (style ? mName + " " + style : mName);
6691 }
6692 if (!flattenSpans || curStyle != style) {
6693 while (curStart < stream.start) {
6694 curStart = Math.min(stream.start, curStart + 50000);
6695 f(curStart, curStyle);
6696 }
6697 curStyle = style;
6698 }
6699 stream.start = stream.pos;
6700 }
6701 while (curStart < stream.pos) {
6702 // Webkit seems to refuse to render text nodes longer than 57444 characters
6703 var pos = Math.min(stream.pos, curStart + 50000);
6704 f(pos, curStyle);
6705 curStart = pos;
6706 }
6707 }
6708
6709 // Compute a style array (an array starting with a mode generation
6710 // -- for invalidation -- followed by pairs of end positions and
6711 // style strings), which is used to highlight the tokens on the
6712 // line.
6713 function highlightLine(cm, line, state, forceToEnd) {
6714 // A styles array always starts with a number identifying the
6715 // mode/overlays that it is based on (for easy invalidation).
6716 var st = [cm.state.modeGen], lineClasses = {};
6717 // Compute the base array of styles
6718 runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
6719 st.push(end, style);
6720 }, lineClasses, forceToEnd);
6721
6722 // Run overlays, adjust style array.
6723 for (var o = 0; o < cm.state.overlays.length; ++o) {
6724 var overlay = cm.state.overlays[o], i = 1, at = 0;
6725 runMode(cm, line.text, overlay.mode, true, function(end, style) {
6726 var start = i;
6727 // Ensure there's a token end at the current position, and that i points at it
6728 while (at < end) {
6729 var i_end = st[i];
6730 if (i_end > end)
6731 st.splice(i, 1, end, st[i+1], i_end);
6732 i += 2;
6733 at = Math.min(end, i_end);
6734 }
6735 if (!style) return;
6736 if (overlay.opaque) {
6737 st.splice(start, i - start, end, "cm-overlay " + style);
6738 i = start + 2;
6739 } else {
6740 for (; start < i; start += 2) {
6741 var cur = st[start+1];
6742 st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style;
6743 }
6744 }
6745 }, lineClasses);
6746 }
6747
6748 return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null};
6749 }
6750
6751 function getLineStyles(cm, line, updateFrontier) {
6752 if (!line.styles || line.styles[0] != cm.state.modeGen) {
6753 var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line)));
6754 line.styles = result.styles;
6755 if (result.classes) line.styleClasses = result.classes;
6756 else if (line.styleClasses) line.styleClasses = null;
6757 if (updateFrontier === cm.doc.frontier) cm.doc.frontier++;
6758 }
6759 return line.styles;
6760 }
6761
6762 // Lightweight form of highlight -- proceed over this line and
6763 // update state, but don't save a style array. Used for lines that
6764 // aren't currently visible.
6765 function processLine(cm, text, state, startAt) {
6766 var mode = cm.doc.mode;
6767 var stream = new StringStream(text, cm.options.tabSize);
6768 stream.start = stream.pos = startAt || 0;
6769 if (text == "") callBlankLine(mode, state);
6770 while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) {
6771 readToken(mode, stream, state);
6772 stream.start = stream.pos;
6773 }
6774 }
6775
6776 // Convert a style as returned by a mode (either null, or a string
6777 // containing one or more styles) to a CSS style. This is cached,
6778 // and also looks for line-wide styles.
6779 var styleToClassCache = {}, styleToClassCacheWithMode = {};
6780 function interpretTokenStyle(style, options) {
6781 if (!style || /^\s*$/.test(style)) return null;
6782 var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
6783 return cache[style] ||
6784 (cache[style] = style.replace(/\S+/g, "cm-$&"));
6785 }
6786
6787 // Render the DOM representation of the text of a line. Also builds
6788 // up a 'line map', which points at the DOM nodes that represent
6789 // specific stretches of text, and is used by the measuring code.
6790 // The returned object contains the DOM node, this map, and
6791 // information about line-wide styles that were set by the mode.
6792 function buildLineContent(cm, lineView) {
6793 // The padding-right forces the element to have a 'border', which
6794 // is needed on Webkit to be able to get line-level bounding
6795 // rectangles for it (in measureChar).
6796 var content = elt("span", null, null, webkit ? "padding-right: .1px" : null);
6797 var builder = {pre: elt("pre", [content]), content: content,
6798 col: 0, pos: 0, cm: cm,
6799 splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")};
6800 lineView.measure = {};
6801
6802 // Iterate over the logical lines that make up this visual line.
6803 for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {
6804 var line = i ? lineView.rest[i - 1] : lineView.line, order;
6805 builder.pos = 0;
6806 builder.addToken = buildToken;
6807 // Optionally wire in some hacks into the token-rendering
6808 // algorithm, to deal with browser quirks.
6809 if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))
6810 builder.addToken = buildTokenBadBidi(builder.addToken, order);
6811 builder.map = [];
6812 var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line);
6813 insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate));
6814 if (line.styleClasses) {
6815 if (line.styleClasses.bgClass)
6816 builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "");
6817 if (line.styleClasses.textClass)
6818 builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "");
6819 }
6820
6821 // Ensure at least a single node is present, for measuring.
6822 if (builder.map.length == 0)
6823 builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure)));
6824
6825 // Store the map and a cache object for the current logical line
6826 if (i == 0) {
6827 lineView.measure.map = builder.map;
6828 lineView.measure.cache = {};
6829 } else {
6830 (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map);
6831 (lineView.measure.caches || (lineView.measure.caches = [])).push({});
6832 }
6833 }
6834
6835 // See issue #2901
6836 if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className))
6837 builder.content.className = "cm-tab-wrap-hack";
6838
6839 signal(cm, "renderLine", cm, lineView.line, builder.pre);
6840 if (builder.pre.className)
6841 builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
6842
6843 return builder;
6844 }
6845
6846 function defaultSpecialCharPlaceholder(ch) {
6847 var token = elt("span", "\u2022", "cm-invalidchar");
6848 token.title = "\\u" + ch.charCodeAt(0).toString(16);
6849 token.setAttribute("aria-label", token.title);
6850 return token;
6851 }
6852
6853 // Build up the DOM representation for a single token, and add it to
6854 // the line map. Takes care to render special characters separately.
6855 function buildToken(builder, text, style, startStyle, endStyle, title, css) {
6856 if (!text) return;
6857 var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text;
6858 var special = builder.cm.state.specialChars, mustWrap = false;
6859 if (!special.test(text)) {
6860 builder.col += text.length;
6861 var content = document.createTextNode(displayText);
6862 builder.map.push(builder.pos, builder.pos + text.length, content);
6863 if (ie && ie_version < 9) mustWrap = true;
6864 builder.pos += text.length;
6865 } else {
6866 var content = document.createDocumentFragment(), pos = 0;
6867 while (true) {
6868 special.lastIndex = pos;
6869 var m = special.exec(text);
6870 var skipped = m ? m.index - pos : text.length - pos;
6871 if (skipped) {
6872 var txt = document.createTextNode(displayText.slice(pos, pos + skipped));
6873 if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
6874 else content.appendChild(txt);
6875 builder.map.push(builder.pos, builder.pos + skipped, txt);
6876 builder.col += skipped;
6877 builder.pos += skipped;
6878 }
6879 if (!m) break;
6880 pos += skipped + 1;
6881 if (m[0] == "\t") {
6882 var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
6883 var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
6884 txt.setAttribute("role", "presentation");
6885 txt.setAttribute("cm-text", "\t");
6886 builder.col += tabWidth;
6887 } else {
6888 var txt = builder.cm.options.specialCharPlaceholder(m[0]);
6889 txt.setAttribute("cm-text", m[0]);
6890 if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
6891 else content.appendChild(txt);
6892 builder.col += 1;
6893 }
6894 builder.map.push(builder.pos, builder.pos + 1, txt);
6895 builder.pos++;
6896 }
6897 }
6898 if (style || startStyle || endStyle || mustWrap || css) {
6899 var fullStyle = style || "";
6900 if (startStyle) fullStyle += startStyle;
6901 if (endStyle) fullStyle += endStyle;
6902 var token = elt("span", [content], fullStyle, css);
6903 if (title) token.title = title;
6904 return builder.content.appendChild(token);
6905 }
6906 builder.content.appendChild(content);
6907 }
6908
6909 function splitSpaces(old) {
6910 var out = " ";
6911 for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
6912 out += " ";
6913 return out;
6914 }
6915
6916 // Work around nonsense dimensions being reported for stretches of
6917 // right-to-left text.
6918 function buildTokenBadBidi(inner, order) {
6919 return function(builder, text, style, startStyle, endStyle, title, css) {
6920 style = style ? style + " cm-force-border" : "cm-force-border";
6921 var start = builder.pos, end = start + text.length;
6922 for (;;) {
6923 // Find the part that overlaps with the start of this text
6924 for (var i = 0; i < order.length; i++) {
6925 var part = order[i];
6926 if (part.to > start && part.from <= start) break;
6927 }
6928 if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css);
6929 inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css);
6930 startStyle = null;
6931 text = text.slice(part.to - start);
6932 start = part.to;
6933 }
6934 };
6935 }
6936
6937 function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
6938 var widget = !ignoreWidget && marker.widgetNode;
6939 if (widget) builder.map.push(builder.pos, builder.pos + size, widget);
6940 if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
6941 if (!widget)
6942 widget = builder.content.appendChild(document.createElement("span"));
6943 widget.setAttribute("cm-marker", marker.id);
6944 }
6945 if (widget) {
6946 builder.cm.display.input.setUneditable(widget);
6947 builder.content.appendChild(widget);
6948 }
6949 builder.pos += size;
6950 }
6951
6952 // Outputs a number of spans to make up a line, taking highlighting
6953 // and marked text into account.
6954 function insertLineContent(line, builder, styles) {
6955 var spans = line.markedSpans, allText = line.text, at = 0;
6956 if (!spans) {
6957 for (var i = 1; i < styles.length; i+=2)
6958 builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options));
6959 return;
6960 }
6961
6962 var len = allText.length, pos = 0, i = 1, text = "", style, css;
6963 var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
6964 for (;;) {
6965 if (nextChange == pos) { // Update current marker set
6966 spanStyle = spanEndStyle = spanStartStyle = title = css = "";
6967 collapsed = null; nextChange = Infinity;
6968 var foundBookmarks = [];
6969 for (var j = 0; j < spans.length; ++j) {
6970 var sp = spans[j], m = sp.marker;
6971 if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
6972 foundBookmarks.push(m);
6973 } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
6974 if (sp.to != null && sp.to != pos && nextChange > sp.to) {
6975 nextChange = sp.to;
6976 spanEndStyle = "";
6977 }
6978 if (m.className) spanStyle += " " + m.className;
6979 if (m.css) css = m.css;
6980 if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
6981 if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
6982 if (m.title && !title) title = m.title;
6983 if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
6984 collapsed = sp;
6985 } else if (sp.from > pos && nextChange > sp.from) {
6986 nextChange = sp.from;
6987 }
6988 }
6989 if (collapsed && (collapsed.from || 0) == pos) {
6990 buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
6991 collapsed.marker, collapsed.from == null);
6992 if (collapsed.to == null) return;
6993 if (collapsed.to == pos) collapsed = false;
6994 }
6995 if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j)
6996 buildCollapsedSpan(builder, 0, foundBookmarks[j]);
6997 }
6998 if (pos >= len) break;
6999
7000 var upto = Math.min(len, nextChange);
7001 while (true) {
7002 if (text) {
7003 var end = pos + text.length;
7004 if (!collapsed) {
7005 var tokenText = end > upto ? text.slice(0, upto - pos) : text;
7006 builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
7007 spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css);
7008 }
7009 if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
7010 pos = end;
7011 spanStartStyle = "";
7012 }
7013 text = allText.slice(at, at = styles[i++]);
7014 style = interpretTokenStyle(styles[i++], builder.cm.options);
7015 }
7016 }
7017 }
7018
7019 // DOCUMENT DATA STRUCTURE
7020
7021 // By default, updates that start and end at the beginning of a line
7022 // are treated specially, in order to make the association of line
7023 // widgets and marker elements with the text behave more intuitive.
7024 function isWholeLineUpdate(doc, change) {
7025 return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" &&
7026 (!doc.cm || doc.cm.options.wholeLineUpdateBefore);
7027 }
7028
7029 // Perform a change on the document data structure.
7030 function updateDoc(doc, change, markedSpans, estimateHeight) {
7031 function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
7032 function update(line, text, spans) {
7033 updateLine(line, text, spans, estimateHeight);
7034 signalLater(line, "change", line, change);
7035 }
7036 function linesFor(start, end) {
7037 for (var i = start, result = []; i < end; ++i)
7038 result.push(new Line(text[i], spansFor(i), estimateHeight));
7039 return result;
7040 }
7041
7042 var from = change.from, to = change.to, text = change.text;
7043 var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
7044 var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
7045
7046 // Adjust the line structure
7047 if (change.full) {
7048 doc.insert(0, linesFor(0, text.length));
7049 doc.remove(text.length, doc.size - text.length);
7050 } else if (isWholeLineUpdate(doc, change)) {
7051 // This is a whole-line replace. Treated specially to make
7052 // sure line objects move the way they are supposed to.
7053 var added = linesFor(0, text.length - 1);
7054 update(lastLine, lastLine.text, lastSpans);
7055 if (nlines) doc.remove(from.line, nlines);
7056 if (added.length) doc.insert(from.line, added);
7057 } else if (firstLine == lastLine) {
7058 if (text.length == 1) {
7059 update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
7060 } else {
7061 var added = linesFor(1, text.length - 1);
7062 added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
7063 update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
7064 doc.insert(from.line + 1, added);
7065 }
7066 } else if (text.length == 1) {
7067 update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
7068 doc.remove(from.line + 1, nlines);
7069 } else {
7070 update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
7071 update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
7072 var added = linesFor(1, text.length - 1);
7073 if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
7074 doc.insert(from.line + 1, added);
7075 }
7076
7077 signalLater(doc, "change", doc, change);
7078 }
7079
7080 // The document is represented as a BTree consisting of leaves, with
7081 // chunk of lines in them, and branches, with up to ten leaves or
7082 // other branch nodes below them. The top node is always a branch
7083 // node, and is the document object itself (meaning it has
7084 // additional methods and properties).
7085 //
7086 // All nodes have parent links. The tree is used both to go from
7087 // line numbers to line objects, and to go from objects to numbers.
7088 // It also indexes by height, and is used to convert between height
7089 // and line object, and to find the total height of the document.
7090 //
7091 // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html
7092
7093 function LeafChunk(lines) {
7094 this.lines = lines;
7095 this.parent = null;
7096 for (var i = 0, height = 0; i < lines.length; ++i) {
7097 lines[i].parent = this;
7098 height += lines[i].height;
7099 }
7100 this.height = height;
7101 }
7102
7103 LeafChunk.prototype = {
7104 chunkSize: function() { return this.lines.length; },
7105 // Remove the n lines at offset 'at'.
7106 removeInner: function(at, n) {
7107 for (var i = at, e = at + n; i < e; ++i) {
7108 var line = this.lines[i];
7109 this.height -= line.height;
7110 cleanUpLine(line);
7111 signalLater(line, "delete");
7112 }
7113 this.lines.splice(at, n);
7114 },
7115 // Helper used to collapse a small branch into a single leaf.
7116 collapse: function(lines) {
7117 lines.push.apply(lines, this.lines);
7118 },
7119 // Insert the given array of lines at offset 'at', count them as
7120 // having the given height.
7121 insertInner: function(at, lines, height) {
7122 this.height += height;
7123 this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
7124 for (var i = 0; i < lines.length; ++i) lines[i].parent = this;
7125 },
7126 // Used to iterate over a part of the tree.
7127 iterN: function(at, n, op) {
7128 for (var e = at + n; at < e; ++at)
7129 if (op(this.lines[at])) return true;
7130 }
7131 };
7132
7133 function BranchChunk(children) {
7134 this.children = children;
7135 var size = 0, height = 0;
7136 for (var i = 0; i < children.length; ++i) {
7137 var ch = children[i];
7138 size += ch.chunkSize(); height += ch.height;
7139 ch.parent = this;
7140 }
7141 this.size = size;
7142 this.height = height;
7143 this.parent = null;
7144 }
7145
7146 BranchChunk.prototype = {
7147 chunkSize: function() { return this.size; },
7148 removeInner: function(at, n) {
7149 this.size -= n;
7150 for (var i = 0; i < this.children.length; ++i) {
7151 var child = this.children[i], sz = child.chunkSize();
7152 if (at < sz) {
7153 var rm = Math.min(n, sz - at), oldHeight = child.height;
7154 child.removeInner(at, rm);
7155 this.height -= oldHeight - child.height;
7156 if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
7157 if ((n -= rm) == 0) break;
7158 at = 0;
7159 } else at -= sz;
7160 }
7161 // If the result is smaller than 25 lines, ensure that it is a
7162 // single leaf node.
7163 if (this.size - n < 25 &&
7164 (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) {
7165 var lines = [];
7166 this.collapse(lines);
7167 this.children = [new LeafChunk(lines)];
7168 this.children[0].parent = this;
7169 }
7170 },
7171 collapse: function(lines) {
7172 for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines);
7173 },
7174 insertInner: function(at, lines, height) {
7175 this.size += lines.length;
7176 this.height += height;
7177 for (var i = 0; i < this.children.length; ++i) {
7178 var child = this.children[i], sz = child.chunkSize();
7179 if (at <= sz) {
7180 child.insertInner(at, lines, height);
7181 if (child.lines && child.lines.length > 50) {
7182 while (child.lines.length > 50) {
7183 var spilled = child.lines.splice(child.lines.length - 25, 25);
7184 var newleaf = new LeafChunk(spilled);
7185 child.height -= newleaf.height;
7186 this.children.splice(i + 1, 0, newleaf);
7187 newleaf.parent = this;
7188 }
7189 this.maybeSpill();
7190 }
7191 break;
7192 }
7193 at -= sz;
7194 }
7195 },
7196 // When a node has grown, check whether it should be split.
7197 maybeSpill: function() {
7198 if (this.children.length <= 10) return;
7199 var me = this;
7200 do {
7201 var spilled = me.children.splice(me.children.length - 5, 5);
7202 var sibling = new BranchChunk(spilled);
7203 if (!me.parent) { // Become the parent node
7204 var copy = new BranchChunk(me.children);
7205 copy.parent = me;
7206 me.children = [copy, sibling];
7207 me = copy;
7208 } else {
7209 me.size -= sibling.size;
7210 me.height -= sibling.height;
7211 var myIndex = indexOf(me.parent.children, me);
7212 me.parent.children.splice(myIndex + 1, 0, sibling);
7213 }
7214 sibling.parent = me.parent;
7215 } while (me.children.length > 10);
7216 me.parent.maybeSpill();
7217 },
7218 iterN: function(at, n, op) {
7219 for (var i = 0; i < this.children.length; ++i) {
7220 var child = this.children[i], sz = child.chunkSize();
7221 if (at < sz) {
7222 var used = Math.min(n, sz - at);
7223 if (child.iterN(at, used, op)) return true;
7224 if ((n -= used) == 0) break;
7225 at = 0;
7226 } else at -= sz;
7227 }
7228 }
7229 };
7230
7231 var nextDocId = 0;
7232 var Doc = CodeMirror.Doc = function(text, mode, firstLine) {
7233 if (!(this instanceof Doc)) return new Doc(text, mode, firstLine);
7234 if (firstLine == null) firstLine = 0;
7235
7236 BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
7237 this.first = firstLine;
7238 this.scrollTop = this.scrollLeft = 0;
7239 this.cantEdit = false;
7240 this.cleanGeneration = 1;
7241 this.frontier = firstLine;
7242 var start = Pos(firstLine, 0);
7243 this.sel = simpleSelection(start);
7244 this.history = new History(null);
7245 this.id = ++nextDocId;
7246 this.modeOption = mode;
7247
7248 if (typeof text == "string") text = splitLines(text);
7249 updateDoc(this, {from: start, to: start, text: text});
7250 setSelection(this, simpleSelection(start), sel_dontScroll);
7251 };
7252
7253 Doc.prototype = createObj(BranchChunk.prototype, {
7254 constructor: Doc,
7255 // Iterate over the document. Supports two forms -- with only one
7256 // argument, it calls that for each line in the document. With
7257 // three, it iterates over the range given by the first two (with
7258 // the second being non-inclusive).
7259 iter: function(from, to, op) {
7260 if (op) this.iterN(from - this.first, to - from, op);
7261 else this.iterN(this.first, this.first + this.size, from);
7262 },
7263
7264 // Non-public interface for adding and removing lines.
7265 insert: function(at, lines) {
7266 var height = 0;
7267 for (var i = 0; i < lines.length; ++i) height += lines[i].height;
7268 this.insertInner(at - this.first, lines, height);
7269 },
7270 remove: function(at, n) { this.removeInner(at - this.first, n); },
7271
7272 // From here, the methods are part of the public interface. Most
7273 // are also available from CodeMirror (editor) instances.
7274
7275 getValue: function(lineSep) {
7276 var lines = getLines(this, this.first, this.first + this.size);
7277 if (lineSep === false) return lines;
7278 return lines.join(lineSep || "\n");
7279 },
7280 setValue: docMethodOp(function(code) {
7281 var top = Pos(this.first, 0), last = this.first + this.size - 1;
7282 makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
7283 text: splitLines(code), origin: "setValue", full: true}, true);
7284 setSelection(this, simpleSelection(top));
7285 }),
7286 replaceRange: function(code, from, to, origin) {
7287 from = clipPos(this, from);
7288 to = to ? clipPos(this, to) : from;
7289 replaceRange(this, code, from, to, origin);
7290 },
7291 getRange: function(from, to, lineSep) {
7292 var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
7293 if (lineSep === false) return lines;
7294 return lines.join(lineSep || "\n");
7295 },
7296
7297 getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
7298
7299 getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
7300 getLineNumber: function(line) {return lineNo(line);},
7301
7302 getLineHandleVisualStart: function(line) {
7303 if (typeof line == "number") line = getLine(this, line);
7304 return visualLine(line);
7305 },
7306
7307 lineCount: function() {return this.size;},
7308 firstLine: function() {return this.first;},
7309 lastLine: function() {return this.first + this.size - 1;},
7310
7311 clipPos: function(pos) {return clipPos(this, pos);},
7312
7313 getCursor: function(start) {
7314 var range = this.sel.primary(), pos;
7315 if (start == null || start == "head") pos = range.head;
7316 else if (start == "anchor") pos = range.anchor;
7317 else if (start == "end" || start == "to" || start === false) pos = range.to();
7318 else pos = range.from();
7319 return pos;
7320 },
7321 listSelections: function() { return this.sel.ranges; },
7322 somethingSelected: function() {return this.sel.somethingSelected();},
7323
7324 setCursor: docMethodOp(function(line, ch, options) {
7325 setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options);
7326 }),
7327 setSelection: docMethodOp(function(anchor, head, options) {
7328 setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options);
7329 }),
7330 extendSelection: docMethodOp(function(head, other, options) {
7331 extendSelection(this, clipPos(this, head), other && clipPos(this, other), options);
7332 }),
7333 extendSelections: docMethodOp(function(heads, options) {
7334 extendSelections(this, clipPosArray(this, heads, options));
7335 }),
7336 extendSelectionsBy: docMethodOp(function(f, options) {
7337 extendSelections(this, map(this.sel.ranges, f), options);
7338 }),
7339 setSelections: docMethodOp(function(ranges, primary, options) {
7340 if (!ranges.length) return;
7341 for (var i = 0, out = []; i < ranges.length; i++)
7342 out[i] = new Range(clipPos(this, ranges[i].anchor),
7343 clipPos(this, ranges[i].head));
7344 if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex);
7345 setSelection(this, normalizeSelection(out, primary), options);
7346 }),
7347 addSelection: docMethodOp(function(anchor, head, options) {
7348 var ranges = this.sel.ranges.slice(0);
7349 ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor)));
7350 setSelection(this, normalizeSelection(ranges, ranges.length - 1), options);
7351 }),
7352
7353 getSelection: function(lineSep) {
7354 var ranges = this.sel.ranges, lines;
7355 for (var i = 0; i < ranges.length; i++) {
7356 var sel = getBetween(this, ranges[i].from(), ranges[i].to());
7357 lines = lines ? lines.concat(sel) : sel;
7358 }
7359 if (lineSep === false) return lines;
7360 else return lines.join(lineSep || "\n");
7361 },
7362 getSelections: function(lineSep) {
7363 var parts = [], ranges = this.sel.ranges;
7364 for (var i = 0; i < ranges.length; i++) {
7365 var sel = getBetween(this, ranges[i].from(), ranges[i].to());
7366 if (lineSep !== false) sel = sel.join(lineSep || "\n");
7367 parts[i] = sel;
7368 }
7369 return parts;
7370 },
7371 replaceSelection: function(code, collapse, origin) {
7372 var dup = [];
7373 for (var i = 0; i < this.sel.ranges.length; i++)
7374 dup[i] = code;
7375 this.replaceSelections(dup, collapse, origin || "+input");
7376 },
7377 replaceSelections: docMethodOp(function(code, collapse, origin) {
7378 var changes = [], sel = this.sel;
7379 for (var i = 0; i < sel.ranges.length; i++) {
7380 var range = sel.ranges[i];
7381 changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin};
7382 }
7383 var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse);
7384 for (var i = changes.length - 1; i >= 0; i--)
7385 makeChange(this, changes[i]);
7386 if (newSel) setSelectionReplaceHistory(this, newSel);
7387 else if (this.cm) ensureCursorVisible(this.cm);
7388 }),
7389 undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}),
7390 redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}),
7391 undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}),
7392 redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}),
7393
7394 setExtending: function(val) {this.extend = val;},
7395 getExtending: function() {return this.extend;},
7396
7397 historySize: function() {
7398 var hist = this.history, done = 0, undone = 0;
7399 for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done;
7400 for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone;
7401 return {undo: done, redo: undone};
7402 },
7403 clearHistory: function() {this.history = new History(this.history.maxGeneration);},
7404
7405 markClean: function() {
7406 this.cleanGeneration = this.changeGeneration(true);
7407 },
7408 changeGeneration: function(forceSplit) {
7409 if (forceSplit)
7410 this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
7411 return this.history.generation;
7412 },
7413 isClean: function (gen) {
7414 return this.history.generation == (gen || this.cleanGeneration);
7415 },
7416
7417 getHistory: function() {
7418 return {done: copyHistoryArray(this.history.done),
7419 undone: copyHistoryArray(this.history.undone)};
7420 },
7421 setHistory: function(histData) {
7422 var hist = this.history = new History(this.history.maxGeneration);
7423 hist.done = copyHistoryArray(histData.done.slice(0), null, true);
7424 hist.undone = copyHistoryArray(histData.undone.slice(0), null, true);
7425 },
7426
7427 addLineClass: docMethodOp(function(handle, where, cls) {
7428 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
7429 var prop = where == "text" ? "textClass"
7430 : where == "background" ? "bgClass"
7431 : where == "gutter" ? "gutterClass" : "wrapClass";
7432 if (!line[prop]) line[prop] = cls;
7433 else if (classTest(cls).test(line[prop])) return false;
7434 else line[prop] += " " + cls;
7435 return true;
7436 });
7437 }),
7438 removeLineClass: docMethodOp(function(handle, where, cls) {
7439 return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) {
7440 var prop = where == "text" ? "textClass"
7441 : where == "background" ? "bgClass"
7442 : where == "gutter" ? "gutterClass" : "wrapClass";
7443 var cur = line[prop];
7444 if (!cur) return false;
7445 else if (cls == null) line[prop] = null;
7446 else {
7447 var found = cur.match(classTest(cls));
7448 if (!found) return false;
7449 var end = found.index + found[0].length;
7450 line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
7451 }
7452 return true;
7453 });
7454 }),
7455
7456 addLineWidget: docMethodOp(function(handle, node, options) {
7457 return addLineWidget(this, handle, node, options);
7458 }),
7459 removeLineWidget: function(widget) { widget.clear(); },
7460
7461 markText: function(from, to, options) {
7462 return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
7463 },
7464 setBookmark: function(pos, options) {
7465 var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
7466 insertLeft: options && options.insertLeft,
7467 clearWhenEmpty: false, shared: options && options.shared,
7468 handleMouseEvents: options && options.handleMouseEvents};
7469 pos = clipPos(this, pos);
7470 return markText(this, pos, pos, realOpts, "bookmark");
7471 },
7472 findMarksAt: function(pos) {
7473 pos = clipPos(this, pos);
7474 var markers = [], spans = getLine(this, pos.line).markedSpans;
7475 if (spans) for (var i = 0; i < spans.length; ++i) {
7476 var span = spans[i];
7477 if ((span.from == null || span.from <= pos.ch) &&
7478 (span.to == null || span.to >= pos.ch))
7479 markers.push(span.marker.parent || span.marker);
7480 }
7481 return markers;
7482 },
7483 findMarks: function(from, to, filter) {
7484 from = clipPos(this, from); to = clipPos(this, to);
7485 var found = [], lineNo = from.line;
7486 this.iter(from.line, to.line + 1, function(line) {
7487 var spans = line.markedSpans;
7488 if (spans) for (var i = 0; i < spans.length; i++) {
7489 var span = spans[i];
7490 if (!(lineNo == from.line && from.ch > span.to ||
7491 span.from == null && lineNo != from.line||
7492 lineNo == to.line && span.from > to.ch) &&
7493 (!filter || filter(span.marker)))
7494 found.push(span.marker.parent || span.marker);
7495 }
7496 ++lineNo;
7497 });
7498 return found;
7499 },
7500 getAllMarks: function() {
7501 var markers = [];
7502 this.iter(function(line) {
7503 var sps = line.markedSpans;
7504 if (sps) for (var i = 0; i < sps.length; ++i)
7505 if (sps[i].from != null) markers.push(sps[i].marker);
7506 });
7507 return markers;
7508 },
7509
7510 posFromIndex: function(off) {
7511 var ch, lineNo = this.first;
7512 this.iter(function(line) {
7513 var sz = line.text.length + 1;
7514 if (sz > off) { ch = off; return true; }
7515 off -= sz;
7516 ++lineNo;
7517 });
7518 return clipPos(this, Pos(lineNo, ch));
7519 },
7520 indexFromPos: function (coords) {
7521 coords = clipPos(this, coords);
7522 var index = coords.ch;
7523 if (coords.line < this.first || coords.ch < 0) return 0;
7524 this.iter(this.first, coords.line, function (line) {
7525 index += line.text.length + 1;
7526 });
7527 return index;
7528 },
7529
7530 copy: function(copyHistory) {
7531 var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first);
7532 doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
7533 doc.sel = this.sel;
7534 doc.extend = false;
7535 if (copyHistory) {
7536 doc.history.undoDepth = this.history.undoDepth;
7537 doc.setHistory(this.getHistory());
7538 }
7539 return doc;
7540 },
7541
7542 linkedDoc: function(options) {
7543 if (!options) options = {};
7544 var from = this.first, to = this.first + this.size;
7545 if (options.from != null && options.from > from) from = options.from;
7546 if (options.to != null && options.to < to) to = options.to;
7547 var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from);
7548 if (options.sharedHist) copy.history = this.history;
7549 (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
7550 copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
7551 copySharedMarkers(copy, findSharedMarkers(this));
7552 return copy;
7553 },
7554 unlinkDoc: function(other) {
7555 if (other instanceof CodeMirror) other = other.doc;
7556 if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
7557 var link = this.linked[i];
7558 if (link.doc != other) continue;
7559 this.linked.splice(i, 1);
7560 other.unlinkDoc(this);
7561 detachSharedMarkers(findSharedMarkers(this));
7562 break;
7563 }
7564 // If the histories were shared, split them again
7565 if (other.history == this.history) {
7566 var splitIds = [other.id];
7567 linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
7568 other.history = new History(null);
7569 other.history.done = copyHistoryArray(this.history.done, splitIds);
7570 other.history.undone = copyHistoryArray(this.history.undone, splitIds);
7571 }
7572 },
7573 iterLinkedDocs: function(f) {linkedDocs(this, f);},
7574
7575 getMode: function() {return this.mode;},
7576 getEditor: function() {return this.cm;}
7577 });
7578
7579 // Public alias.
7580 Doc.prototype.eachLine = Doc.prototype.iter;
7581
7582 // Set up methods on CodeMirror's prototype to redirect to the editor's document.
7583 var dontDelegate = "iter insert remove copy getEditor".split(" ");
7584 for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
7585 CodeMirror.prototype[prop] = (function(method) {
7586 return function() {return method.apply(this.doc, arguments);};
7587 })(Doc.prototype[prop]);
7588
7589 eventMixin(Doc);
7590
7591 // Call f for all linked documents.
7592 function linkedDocs(doc, f, sharedHistOnly) {
7593 function propagate(doc, skip, sharedHist) {
7594 if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
7595 var rel = doc.linked[i];
7596 if (rel.doc == skip) continue;
7597 var shared = sharedHist && rel.sharedHist;
7598 if (sharedHistOnly && !shared) continue;
7599 f(rel.doc, shared);
7600 propagate(rel.doc, doc, shared);
7601 }
7602 }
7603 propagate(doc, null, true);
7604 }
7605
7606 // Attach a document to an editor.
7607 function attachDoc(cm, doc) {
7608 if (doc.cm) throw new Error("This document is already in use.");
7609 cm.doc = doc;
7610 doc.cm = cm;
7611 estimateLineHeights(cm);
7612 loadMode(cm);
7613 if (!cm.options.lineWrapping) findMaxLine(cm);
7614 cm.options.mode = doc.modeOption;
7615 regChange(cm);
7616 }
7617
7618 // LINE UTILITIES
7619
7620 // Find the line object corresponding to the given line number.
7621 function getLine(doc, n) {
7622 n -= doc.first;
7623 if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document.");
7624 for (var chunk = doc; !chunk.lines;) {
7625 for (var i = 0;; ++i) {
7626 var child = chunk.children[i], sz = child.chunkSize();
7627 if (n < sz) { chunk = child; break; }
7628 n -= sz;
7629 }
7630 }
7631 return chunk.lines[n];
7632 }
7633
7634 // Get the part of a document between two positions, as an array of
7635 // strings.
7636 function getBetween(doc, start, end) {
7637 var out = [], n = start.line;
7638 doc.iter(start.line, end.line + 1, function(line) {
7639 var text = line.text;
7640 if (n == end.line) text = text.slice(0, end.ch);
7641 if (n == start.line) text = text.slice(start.ch);
7642 out.push(text);
7643 ++n;
7644 });
7645 return out;
7646 }
7647 // Get the lines between from and to, as array of strings.
7648 function getLines(doc, from, to) {
7649 var out = [];
7650 doc.iter(from, to, function(line) { out.push(line.text); });
7651 return out;
7652 }
7653
7654 // Update the height of a line, propagating the height change
7655 // upwards to parent nodes.
7656 function updateLineHeight(line, height) {
7657 var diff = height - line.height;
7658 if (diff) for (var n = line; n; n = n.parent) n.height += diff;
7659 }
7660
7661 // Given a line object, find its line number by walking up through
7662 // its parent links.
7663 function lineNo(line) {
7664 if (line.parent == null) return null;
7665 var cur = line.parent, no = indexOf(cur.lines, line);
7666 for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
7667 for (var i = 0;; ++i) {
7668 if (chunk.children[i] == cur) break;
7669 no += chunk.children[i].chunkSize();
7670 }
7671 }
7672 return no + cur.first;
7673 }
7674
7675 // Find the line at the given vertical position, using the height
7676 // information in the document tree.
7677 function lineAtHeight(chunk, h) {
7678 var n = chunk.first;
7679 outer: do {
7680 for (var i = 0; i < chunk.children.length; ++i) {
7681 var child = chunk.children[i], ch = child.height;
7682 if (h < ch) { chunk = child; continue outer; }
7683 h -= ch;
7684 n += child.chunkSize();
7685 }
7686 return n;
7687 } while (!chunk.lines);
7688 for (var i = 0; i < chunk.lines.length; ++i) {
7689 var line = chunk.lines[i], lh = line.height;
7690 if (h < lh) break;
7691 h -= lh;
7692 }
7693 return n + i;
7694 }
7695
7696
7697 // Find the height above the given line.
7698 function heightAtLine(lineObj) {
7699 lineObj = visualLine(lineObj);
7700
7701 var h = 0, chunk = lineObj.parent;
7702 for (var i = 0; i < chunk.lines.length; ++i) {
7703 var line = chunk.lines[i];
7704 if (line == lineObj) break;
7705 else h += line.height;
7706 }
7707 for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
7708 for (var i = 0; i < p.children.length; ++i) {
7709 var cur = p.children[i];
7710 if (cur == chunk) break;
7711 else h += cur.height;
7712 }
7713 }
7714 return h;
7715 }
7716
7717 // Get the bidi ordering for the given line (and cache it). Returns
7718 // false for lines that are fully left-to-right, and an array of
7719 // BidiSpan objects otherwise.
7720 function getOrder(line) {
7721 var order = line.order;
7722 if (order == null) order = line.order = bidiOrdering(line.text);
7723 return order;
7724 }
7725
7726 // HISTORY
7727
7728 function History(startGen) {
7729 // Arrays of change events and selections. Doing something adds an
7730 // event to done and clears undo. Undoing moves events from done
7731 // to undone, redoing moves them in the other direction.
7732 this.done = []; this.undone = [];
7733 this.undoDepth = Infinity;
7734 // Used to track when changes can be merged into a single undo
7735 // event
7736 this.lastModTime = this.lastSelTime = 0;
7737 this.lastOp = this.lastSelOp = null;
7738 this.lastOrigin = this.lastSelOrigin = null;
7739 // Used by the isClean() method
7740 this.generation = this.maxGeneration = startGen || 1;
7741 }
7742
7743 // Create a history change event from an updateDoc-style change
7744 // object.
7745 function historyChangeFromChange(doc, change) {
7746 var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
7747 attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
7748 linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
7749 return histChange;
7750 }
7751
7752 // Pop all selection events off the end of a history array. Stop at
7753 // a change event.
7754 function clearSelectionEvents(array) {
7755 while (array.length) {
7756 var last = lst(array);
7757 if (last.ranges) array.pop();
7758 else break;
7759 }
7760 }
7761
7762 // Find the top change event in the history. Pop off selection
7763 // events that are in the way.
7764 function lastChangeEvent(hist, force) {
7765 if (force) {
7766 clearSelectionEvents(hist.done);
7767 return lst(hist.done);
7768 } else if (hist.done.length && !lst(hist.done).ranges) {
7769 return lst(hist.done);
7770 } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) {
7771 hist.done.pop();
7772 return lst(hist.done);
7773 }
7774 }
7775
7776 // Register a change in the history. Merges changes that are within
7777 // a single operation, ore are close together with an origin that
7778 // allows merging (starting with "+") into a single event.
7779 function addChangeToHistory(doc, change, selAfter, opId) {
7780 var hist = doc.history;
7781 hist.undone.length = 0;
7782 var time = +new Date, cur;
7783
7784 if ((hist.lastOp == opId ||
7785 hist.lastOrigin == change.origin && change.origin &&
7786 ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||
7787 change.origin.charAt(0) == "*")) &&
7788 (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
7789 // Merge this change into the last event
7790 var last = lst(cur.changes);
7791 if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) {
7792 // Optimized case for simple insertion -- don't want to add
7793 // new changesets for every character typed
7794 last.to = changeEnd(change);
7795 } else {
7796 // Add new sub-event
7797 cur.changes.push(historyChangeFromChange(doc, change));
7798 }
7799 } else {
7800 // Can not be merged, start a new event.
7801 var before = lst(hist.done);
7802 if (!before || !before.ranges)
7803 pushSelectionToHistory(doc.sel, hist.done);
7804 cur = {changes: [historyChangeFromChange(doc, change)],
7805 generation: hist.generation};
7806 hist.done.push(cur);
7807 while (hist.done.length > hist.undoDepth) {
7808 hist.done.shift();
7809 if (!hist.done[0].ranges) hist.done.shift();
7810 }
7811 }
7812 hist.done.push(selAfter);
7813 hist.generation = ++hist.maxGeneration;
7814 hist.lastModTime = hist.lastSelTime = time;
7815 hist.lastOp = hist.lastSelOp = opId;
7816 hist.lastOrigin = hist.lastSelOrigin = change.origin;
7817
7818 if (!last) signal(doc, "historyAdded");
7819 }
7820
7821 function selectionEventCanBeMerged(doc, origin, prev, sel) {
7822 var ch = origin.charAt(0);
7823 return ch == "*" ||
7824 ch == "+" &&
7825 prev.ranges.length == sel.ranges.length &&
7826 prev.somethingSelected() == sel.somethingSelected() &&
7827 new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500);
7828 }
7829
7830 // Called whenever the selection changes, sets the new selection as
7831 // the pending selection in the history, and pushes the old pending
7832 // selection into the 'done' array when it was significantly
7833 // different (in number of selected ranges, emptiness, or time).
7834 function addSelectionToHistory(doc, sel, opId, options) {
7835 var hist = doc.history, origin = options && options.origin;
7836
7837 // A new event is started when the previous origin does not match
7838 // the current, or the origins don't allow matching. Origins
7839 // starting with * are always merged, those starting with + are
7840 // merged when similar and close together in time.
7841 if (opId == hist.lastSelOp ||
7842 (origin && hist.lastSelOrigin == origin &&
7843 (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||
7844 selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))
7845 hist.done[hist.done.length - 1] = sel;
7846 else
7847 pushSelectionToHistory(sel, hist.done);
7848
7849 hist.lastSelTime = +new Date;
7850 hist.lastSelOrigin = origin;
7851 hist.lastSelOp = opId;
7852 if (options && options.clearRedo !== false)
7853 clearSelectionEvents(hist.undone);
7854 }
7855
7856 function pushSelectionToHistory(sel, dest) {
7857 var top = lst(dest);
7858 if (!(top && top.ranges && top.equals(sel)))
7859 dest.push(sel);
7860 }
7861
7862 // Used to store marked span information in the history.
7863 function attachLocalSpans(doc, change, from, to) {
7864 var existing = change["spans_" + doc.id], n = 0;
7865 doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
7866 if (line.markedSpans)
7867 (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
7868 ++n;
7869 });
7870 }
7871
7872 // When un/re-doing restores text containing marked spans, those
7873 // that have been explicitly cleared should not be restored.
7874 function removeClearedSpans(spans) {
7875 if (!spans) return null;
7876 for (var i = 0, out; i < spans.length; ++i) {
7877 if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
7878 else if (out) out.push(spans[i]);
7879 }
7880 return !out ? spans : out.length ? out : null;
7881 }
7882
7883 // Retrieve and filter the old marked spans stored in a change event.
7884 function getOldSpans(doc, change) {
7885 var found = change["spans_" + doc.id];
7886 if (!found) return null;
7887 for (var i = 0, nw = []; i < change.text.length; ++i)
7888 nw.push(removeClearedSpans(found[i]));
7889 return nw;
7890 }
7891
7892 // Used both to provide a JSON-safe object in .getHistory, and, when
7893 // detaching a document, to split the history in two
7894 function copyHistoryArray(events, newGroup, instantiateSel) {
7895 for (var i = 0, copy = []; i < events.length; ++i) {
7896 var event = events[i];
7897 if (event.ranges) {
7898 copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event);
7899 continue;
7900 }
7901 var changes = event.changes, newChanges = [];
7902 copy.push({changes: newChanges});
7903 for (var j = 0; j < changes.length; ++j) {
7904 var change = changes[j], m;
7905 newChanges.push({from: change.from, to: change.to, text: change.text});
7906 if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
7907 if (indexOf(newGroup, Number(m[1])) > -1) {
7908 lst(newChanges)[prop] = change[prop];
7909 delete change[prop];
7910 }
7911 }
7912 }
7913 }
7914 return copy;
7915 }
7916
7917 // Rebasing/resetting history to deal with externally-sourced changes
7918
7919 function rebaseHistSelSingle(pos, from, to, diff) {
7920 if (to < pos.line) {
7921 pos.line += diff;
7922 } else if (from < pos.line) {
7923 pos.line = from;
7924 pos.ch = 0;
7925 }
7926 }
7927
7928 // Tries to rebase an array of history events given a change in the
7929 // document. If the change touches the same lines as the event, the
7930 // event, and everything 'behind' it, is discarded. If the change is
7931 // before the event, the event's positions are updated. Uses a
7932 // copy-on-write scheme for the positions, to avoid having to
7933 // reallocate them all on every rebase, but also avoid problems with
7934 // shared position objects being unsafely updated.
7935 function rebaseHistArray(array, from, to, diff) {
7936 for (var i = 0; i < array.length; ++i) {
7937 var sub = array[i], ok = true;
7938 if (sub.ranges) {
7939 if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; }
7940 for (var j = 0; j < sub.ranges.length; j++) {
7941 rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff);
7942 rebaseHistSelSingle(sub.ranges[j].head, from, to, diff);
7943 }
7944 continue;
7945 }
7946 for (var j = 0; j < sub.changes.length; ++j) {
7947 var cur = sub.changes[j];
7948 if (to < cur.from.line) {
7949 cur.from = Pos(cur.from.line + diff, cur.from.ch);
7950 cur.to = Pos(cur.to.line + diff, cur.to.ch);
7951 } else if (from <= cur.to.line) {
7952 ok = false;
7953 break;
7954 }
7955 }
7956 if (!ok) {
7957 array.splice(0, i + 1);
7958 i = 0;
7959 }
7960 }
7961 }
7962
7963 function rebaseHist(hist, change) {
7964 var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
7965 rebaseHistArray(hist.done, from, to, diff);
7966 rebaseHistArray(hist.undone, from, to, diff);
7967 }
7968
7969 // EVENT UTILITIES
7970
7971 // Due to the fact that we still support jurassic IE versions, some
7972 // compatibility wrappers are needed.
7973
7974 var e_preventDefault = CodeMirror.e_preventDefault = function(e) {
7975 if (e.preventDefault) e.preventDefault();
7976 else e.returnValue = false;
7977 };
7978 var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) {
7979 if (e.stopPropagation) e.stopPropagation();
7980 else e.cancelBubble = true;
7981 };
7982 function e_defaultPrevented(e) {
7983 return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false;
7984 }
7985 var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);};
7986
7987 function e_target(e) {return e.target || e.srcElement;}
7988 function e_button(e) {
7989 var b = e.which;
7990 if (b == null) {
7991 if (e.button & 1) b = 1;
7992 else if (e.button & 2) b = 3;
7993 else if (e.button & 4) b = 2;
7994 }
7995 if (mac && e.ctrlKey && b == 1) b = 3;
7996 return b;
7997 }
7998
7999 // EVENT HANDLING
8000
8001 // Lightweight event framework. on/off also work on DOM nodes,
8002 // registering native DOM handlers.
8003
8004 var on = CodeMirror.on = function(emitter, type, f) {
8005 if (emitter.addEventListener)
8006 emitter.addEventListener(type, f, false);
8007 else if (emitter.attachEvent)
8008 emitter.attachEvent("on" + type, f);
8009 else {
8010 var map = emitter._handlers || (emitter._handlers = {});
8011 var arr = map[type] || (map[type] = []);
8012 arr.push(f);
8013 }
8014 };
8015
8016 var off = CodeMirror.off = function(emitter, type, f) {
8017 if (emitter.removeEventListener)
8018 emitter.removeEventListener(type, f, false);
8019 else if (emitter.detachEvent)
8020 emitter.detachEvent("on" + type, f);
8021 else {
8022 var arr = emitter._handlers && emitter._handlers[type];
8023 if (!arr) return;
8024 for (var i = 0; i < arr.length; ++i)
8025 if (arr[i] == f) { arr.splice(i, 1); break; }
8026 }
8027 };
8028
8029 var signal = CodeMirror.signal = function(emitter, type /*, values...*/) {
8030 var arr = emitter._handlers && emitter._handlers[type];
8031 if (!arr) return;
8032 var args = Array.prototype.slice.call(arguments, 2);
8033 for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
8034 };
8035
8036 var orphanDelayedCallbacks = null;
8037
8038 // Often, we want to signal events at a point where we are in the
8039 // middle of some work, but don't want the handler to start calling
8040 // other methods on the editor, which might be in an inconsistent
8041 // state or simply not expect any other events to happen.
8042 // signalLater looks whether there are any handlers, and schedules
8043 // them to be executed when the last operation ends, or, if no
8044 // operation is active, when a timeout fires.
8045 function signalLater(emitter, type /*, values...*/) {
8046 var arr = emitter._handlers && emitter._handlers[type];
8047 if (!arr) return;
8048 var args = Array.prototype.slice.call(arguments, 2), list;
8049 if (operationGroup) {
8050 list = operationGroup.delayedCallbacks;
8051 } else if (orphanDelayedCallbacks) {
8052 list = orphanDelayedCallbacks;
8053 } else {
8054 list = orphanDelayedCallbacks = [];
8055 setTimeout(fireOrphanDelayed, 0);
8056 }
8057 function bnd(f) {return function(){f.apply(null, args);};};
8058 for (var i = 0; i < arr.length; ++i)
8059 list.push(bnd(arr[i]));
8060 }
8061
8062 function fireOrphanDelayed() {
8063 var delayed = orphanDelayedCallbacks;
8064 orphanDelayedCallbacks = null;
8065 for (var i = 0; i < delayed.length; ++i) delayed[i]();
8066 }
8067
8068 // The DOM events that CodeMirror handles can be overridden by
8069 // registering a (non-DOM) handler on the editor for the event name,
8070 // and preventDefault-ing the event in that handler.
8071 function signalDOMEvent(cm, e, override) {
8072 if (typeof e == "string")
8073 e = {type: e, preventDefault: function() { this.defaultPrevented = true; }};
8074 signal(cm, override || e.type, cm, e);
8075 return e_defaultPrevented(e) || e.codemirrorIgnore;
8076 }
8077
8078 function signalCursorActivity(cm) {
8079 var arr = cm._handlers && cm._handlers.cursorActivity;
8080 if (!arr) return;
8081 var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []);
8082 for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1)
8083 set.push(arr[i]);
8084 }
8085
8086 function hasHandler(emitter, type) {
8087 var arr = emitter._handlers && emitter._handlers[type];
8088 return arr && arr.length > 0;
8089 }
8090
8091 // Add on and off methods to a constructor's prototype, to make
8092 // registering events on such objects more convenient.
8093 function eventMixin(ctor) {
8094 ctor.prototype.on = function(type, f) {on(this, type, f);};
8095 ctor.prototype.off = function(type, f) {off(this, type, f);};
8096 }
8097
8098 // MISC UTILITIES
8099
8100 // Number of pixels added to scroller and sizer to hide scrollbar
8101 var scrollerGap = 30;
8102
8103 // Returned or thrown by various protocols to signal 'I'm not
8104 // handling this'.
8105 var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
8106
8107 // Reused option objects for setSelection & friends
8108 var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"};
8109
8110 function Delayed() {this.id = null;}
8111 Delayed.prototype.set = function(ms, f) {
8112 clearTimeout(this.id);
8113 this.id = setTimeout(f, ms);
8114 };
8115
8116 // Counts the column offset in a string, taking tabs into account.
8117 // Used mostly to find indentation.
8118 var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) {
8119 if (end == null) {
8120 end = string.search(/[^\s\u00a0]/);
8121 if (end == -1) end = string.length;
8122 }
8123 for (var i = startIndex || 0, n = startValue || 0;;) {
8124 var nextTab = string.indexOf("\t", i);
8125 if (nextTab < 0 || nextTab >= end)
8126 return n + (end - i);
8127 n += nextTab - i;
8128 n += tabSize - (n % tabSize);
8129 i = nextTab + 1;
8130 }
8131 };
8132
8133 // The inverse of countColumn -- find the offset that corresponds to
8134 // a particular column.
8135 function findColumn(string, goal, tabSize) {
8136 for (var pos = 0, col = 0;;) {
8137 var nextTab = string.indexOf("\t", pos);
8138 if (nextTab == -1) nextTab = string.length;
8139 var skipped = nextTab - pos;
8140 if (nextTab == string.length || col + skipped >= goal)
8141 return pos + Math.min(skipped, goal - col);
8142 col += nextTab - pos;
8143 col += tabSize - (col % tabSize);
8144 pos = nextTab + 1;
8145 if (col >= goal) return pos;
8146 }
8147 }
8148
8149 var spaceStrs = [""];
8150 function spaceStr(n) {
8151 while (spaceStrs.length <= n)
8152 spaceStrs.push(lst(spaceStrs) + " ");
8153 return spaceStrs[n];
8154 }
8155
8156 function lst(arr) { return arr[arr.length-1]; }
8157
8158 var selectInput = function(node) { node.select(); };
8159 if (ios) // Mobile Safari apparently has a bug where select() is broken.
8160 selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; };
8161 else if (ie) // Suppress mysterious IE10 errors
8162 selectInput = function(node) { try { node.select(); } catch(_e) {} };
8163
8164 function indexOf(array, elt) {
8165 for (var i = 0; i < array.length; ++i)
8166 if (array[i] == elt) return i;
8167 return -1;
8168 }
8169 function map(array, f) {
8170 var out = [];
8171 for (var i = 0; i < array.length; i++) out[i] = f(array[i], i);
8172 return out;
8173 }
8174
8175 function nothing() {}
8176
8177 function createObj(base, props) {
8178 var inst;
8179 if (Object.create) {
8180 inst = Object.create(base);
8181 } else {
8182 nothing.prototype = base;
8183 inst = new nothing();
8184 }
8185 if (props) copyObj(props, inst);
8186 return inst;
8187 };
8188
8189 function copyObj(obj, target, overwrite) {
8190 if (!target) target = {};
8191 for (var prop in obj)
8192 if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
8193 target[prop] = obj[prop];
8194 return target;
8195 }
8196
8197 function bind(f) {
8198 var args = Array.prototype.slice.call(arguments, 1);
8199 return function(){return f.apply(null, args);};
8200 }
8201
8202 var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
8203 var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
8204 return /\w/.test(ch) || ch > "\x80" &&
8205 (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
8206 };
8207 function isWordChar(ch, helper) {
8208 if (!helper) return isWordCharBasic(ch);
8209 if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
8210 return helper.test(ch);
8211 }
8212
8213 function isEmpty(obj) {
8214 for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
8215 return true;
8216 }
8217
8218 // Extending unicode characters. A series of a non-extending char +
8219 // any number of extending chars is treated as a single unit as far
8220 // as editing and measuring is concerned. This is not fully correct,
8221 // since some scripts/fonts/browsers also treat other configurations
8222 // of code points as a group.
8223 var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
8224 function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
8225
8226 // DOM UTILITIES
8227
8228 function elt(tag, content, className, style) {
8229 var e = document.createElement(tag);
8230 if (className) e.className = className;
8231 if (style) e.style.cssText = style;
8232 if (typeof content == "string") e.appendChild(document.createTextNode(content));
8233 else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
8234 return e;
8235 }
8236
8237 var range;
8238 if (document.createRange) range = function(node, start, end, endNode) {
8239 var r = document.createRange();
8240 r.setEnd(endNode || node, end);
8241 r.setStart(node, start);
8242 return r;
8243 };
8244 else range = function(node, start, end) {
8245 var r = document.body.createTextRange();
8246 try { r.moveToElementText(node.parentNode); }
8247 catch(e) { return r; }
8248 r.collapse(true);
8249 r.moveEnd("character", end);
8250 r.moveStart("character", start);
8251 return r;
8252 };
8253
8254 function removeChildren(e) {
8255 for (var count = e.childNodes.length; count > 0; --count)
8256 e.removeChild(e.firstChild);
8257 return e;
8258 }
8259
8260 function removeChildrenAndAdd(parent, e) {
8261 return removeChildren(parent).appendChild(e);
8262 }
8263
8264 var contains = CodeMirror.contains = function(parent, child) {
8265 if (child.nodeType == 3) // Android browser always returns false when child is a textnode
8266 child = child.parentNode;
8267 if (parent.contains)
8268 return parent.contains(child);
8269 do {
8270 if (child.nodeType == 11) child = child.host;
8271 if (child == parent) return true;
8272 } while (child = child.parentNode);
8273 };
8274
8275 function activeElt() { return document.activeElement; }
8276 // Older versions of IE throws unspecified error when touching
8277 // document.activeElement in some cases (during loading, in iframe)
8278 if (ie && ie_version < 11) activeElt = function() {
8279 try { return document.activeElement; }
8280 catch(e) { return document.body; }
8281 };
8282
8283 function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); }
8284 var rmClass = CodeMirror.rmClass = function(node, cls) {
8285 var current = node.className;
8286 var match = classTest(cls).exec(current);
8287 if (match) {
8288 var after = current.slice(match.index + match[0].length);
8289 node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
8290 }
8291 };
8292 var addClass = CodeMirror.addClass = function(node, cls) {
8293 var current = node.className;
8294 if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls;
8295 };
8296 function joinClasses(a, b) {
8297 var as = a.split(" ");
8298 for (var i = 0; i < as.length; i++)
8299 if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i];
8300 return b;
8301 }
8302
8303 // WINDOW-WIDE EVENTS
8304
8305 // These must be handled carefully, because naively registering a
8306 // handler for each editor will cause the editors to never be
8307 // garbage collected.
8308
8309 function forEachCodeMirror(f) {
8310 if (!document.body.getElementsByClassName) return;
8311 var byClass = document.body.getElementsByClassName("CodeMirror");
8312 for (var i = 0; i < byClass.length; i++) {
8313 var cm = byClass[i].CodeMirror;
8314 if (cm) f(cm);
8315 }
8316 }
8317
8318 var globalsRegistered = false;
8319 function ensureGlobalHandlers() {
8320 if (globalsRegistered) return;
8321 registerGlobalHandlers();
8322 globalsRegistered = true;
8323 }
8324 function registerGlobalHandlers() {
8325 // When the window resizes, we need to refresh active editors.
8326 var resizeTimer;
8327 on(window, "resize", function() {
8328 if (resizeTimer == null) resizeTimer = setTimeout(function() {
8329 resizeTimer = null;
8330 forEachCodeMirror(onResize);
8331 }, 100);
8332 });
8333 // When the window loses focus, we want to show the editor as blurred
8334 on(window, "blur", function() {
8335 forEachCodeMirror(onBlur);
8336 });
8337 }
8338
8339 // FEATURE DETECTION
8340
8341 // Detect drag-and-drop
8342 var dragAndDrop = function() {
8343 // There is *some* kind of drag-and-drop support in IE6-8, but I
8344 // couldn't get it to work yet.
8345 if (ie && ie_version < 9) return false;
8346 var div = elt('div');
8347 return "draggable" in div || "dragDrop" in div;
8348 }();
8349
8350 var zwspSupported;
8351 function zeroWidthElement(measure) {
8352 if (zwspSupported == null) {
8353 var test = elt("span", "\u200b");
8354 removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
8355 if (measure.firstChild.offsetHeight != 0)
8356 zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8);
8357 }
8358 var node = zwspSupported ? elt("span", "\u200b") :
8359 elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
8360 node.setAttribute("cm-text", "");
8361 return node;
8362 }
8363
8364 // Feature-detect IE's crummy client rect reporting for bidi text
8365 var badBidiRects;
8366 function hasBadBidiRects(measure) {
8367 if (badBidiRects != null) return badBidiRects;
8368 var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA"));
8369 var r0 = range(txt, 0, 1).getBoundingClientRect();
8370 if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780)
8371 var r1 = range(txt, 1, 2).getBoundingClientRect();
8372 return badBidiRects = (r1.right - r0.right < 3);
8373 }
8374
8375 // See if "".split is the broken IE version, if so, provide an
8376 // alternative way to split lines.
8377 var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
8378 var pos = 0, result = [], l = string.length;
8379 while (pos <= l) {
8380 var nl = string.indexOf("\n", pos);
8381 if (nl == -1) nl = string.length;
8382 var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
8383 var rt = line.indexOf("\r");
8384 if (rt != -1) {
8385 result.push(line.slice(0, rt));
8386 pos += rt + 1;
8387 } else {
8388 result.push(line);
8389 pos = nl + 1;
8390 }
8391 }
8392 return result;
8393 } : function(string){return string.split(/\r\n?|\n/);};
8394
8395 var hasSelection = window.getSelection ? function(te) {
8396 try { return te.selectionStart != te.selectionEnd; }
8397 catch(e) { return false; }
8398 } : function(te) {
8399 try {var range = te.ownerDocument.selection.createRange();}
8400 catch(e) {}
8401 if (!range || range.parentElement() != te) return false;
8402 return range.compareEndPoints("StartToEnd", range) != 0;
8403 };
8404
8405 var hasCopyEvent = (function() {
8406 var e = elt("div");
8407 if ("oncopy" in e) return true;
8408 e.setAttribute("oncopy", "return;");
8409 return typeof e.oncopy == "function";
8410 })();
8411
8412 var badZoomedRects = null;
8413 function hasBadZoomedRects(measure) {
8414 if (badZoomedRects != null) return badZoomedRects;
8415 var node = removeChildrenAndAdd(measure, elt("span", "x"));
8416 var normal = node.getBoundingClientRect();
8417 var fromRange = range(node, 0, 1).getBoundingClientRect();
8418 return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
8419 }
8420
8421 // KEY NAMES
8422
8423 var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
8424 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
8425 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
8426 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete",
8427 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
8428 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
8429 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"};
8430 CodeMirror.keyNames = keyNames;
8431 (function() {
8432 // Number keys
8433 for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
8434 // Alphabetic keys
8435 for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
8436 // Function keys
8437 for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
8438 })();
8439
8440 // BIDI HELPERS
8441
8442 function iterateBidiSections(order, from, to, f) {
8443 if (!order) return f(from, to, "ltr");
8444 var found = false;
8445 for (var i = 0; i < order.length; ++i) {
8446 var part = order[i];
8447 if (part.from < to && part.to > from || from == to && part.to == from) {
8448 f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
8449 found = true;
8450 }
8451 }
8452 if (!found) f(from, to, "ltr");
8453 }
8454
8455 function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
8456 function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
8457
8458 function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
8459 function lineRight(line) {
8460 var order = getOrder(line);
8461 if (!order) return line.text.length;
8462 return bidiRight(lst(order));
8463 }
8464
8465 function lineStart(cm, lineN) {
8466 var line = getLine(cm.doc, lineN);
8467 var visual = visualLine(line);
8468 if (visual != line) lineN = lineNo(visual);
8469 var order = getOrder(visual);
8470 var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
8471 return Pos(lineN, ch);
8472 }
8473 function lineEnd(cm, lineN) {
8474 var merged, line = getLine(cm.doc, lineN);
8475 while (merged = collapsedSpanAtEnd(line)) {
8476 line = merged.find(1, true).line;
8477 lineN = null;
8478 }
8479 var order = getOrder(line);
8480 var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
8481 return Pos(lineN == null ? lineNo(line) : lineN, ch);
8482 }
8483 function lineStartSmart(cm, pos) {
8484 var start = lineStart(cm, pos.line);
8485 var line = getLine(cm.doc, start.line);
8486 var order = getOrder(line);
8487 if (!order || order[0].level == 0) {
8488 var firstNonWS = Math.max(0, line.text.search(/\S/));
8489 var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
8490 return Pos(start.line, inWS ? 0 : firstNonWS);
8491 }
8492 return start;
8493 }
8494
8495 function compareBidiLevel(order, a, b) {
8496 var linedir = order[0].level;
8497 if (a == linedir) return true;
8498 if (b == linedir) return false;
8499 return a < b;
8500 }
8501 var bidiOther;
8502 function getBidiPartAt(order, pos) {
8503 bidiOther = null;
8504 for (var i = 0, found; i < order.length; ++i) {
8505 var cur = order[i];
8506 if (cur.from < pos && cur.to > pos) return i;
8507 if ((cur.from == pos || cur.to == pos)) {
8508 if (found == null) {
8509 found = i;
8510 } else if (compareBidiLevel(order, cur.level, order[found].level)) {
8511 if (cur.from != cur.to) bidiOther = found;
8512 return i;
8513 } else {
8514 if (cur.from != cur.to) bidiOther = i;
8515 return found;
8516 }
8517 }
8518 }
8519 return found;
8520 }
8521
8522 function moveInLine(line, pos, dir, byUnit) {
8523 if (!byUnit) return pos + dir;
8524 do pos += dir;
8525 while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
8526 return pos;
8527 }
8528
8529 // This is needed in order to move 'visually' through bi-directional
8530 // text -- i.e., pressing left should make the cursor go left, even
8531 // when in RTL text. The tricky part is the 'jumps', where RTL and
8532 // LTR text touch each other. This often requires the cursor offset
8533 // to move more than one unit, in order to visually move one unit.
8534 function moveVisually(line, start, dir, byUnit) {
8535 var bidi = getOrder(line);
8536 if (!bidi) return moveLogically(line, start, dir, byUnit);
8537 var pos = getBidiPartAt(bidi, start), part = bidi[pos];
8538 var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
8539
8540 for (;;) {
8541 if (target > part.from && target < part.to) return target;
8542 if (target == part.from || target == part.to) {
8543 if (getBidiPartAt(bidi, target) == pos) return target;
8544 part = bidi[pos += dir];
8545 return (dir > 0) == part.level % 2 ? part.to : part.from;
8546 } else {
8547 part = bidi[pos += dir];
8548 if (!part) return null;
8549 if ((dir > 0) == part.level % 2)
8550 target = moveInLine(line, part.to, -1, byUnit);
8551 else
8552 target = moveInLine(line, part.from, 1, byUnit);
8553 }
8554 }
8555 }
8556
8557 function moveLogically(line, start, dir, byUnit) {
8558 var target = start + dir;
8559 if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
8560 return target < 0 || target > line.text.length ? null : target;
8561 }
8562
8563 // Bidirectional ordering algorithm
8564 // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
8565 // that this (partially) implements.
8566
8567 // One-char codes used for character types:
8568 // L (L): Left-to-Right
8569 // R (R): Right-to-Left
8570 // r (AL): Right-to-Left Arabic
8571 // 1 (EN): European Number
8572 // + (ES): European Number Separator
8573 // % (ET): European Number Terminator
8574 // n (AN): Arabic Number
8575 // , (CS): Common Number Separator
8576 // m (NSM): Non-Spacing Mark
8577 // b (BN): Boundary Neutral
8578 // s (B): Paragraph Separator
8579 // t (S): Segment Separator
8580 // w (WS): Whitespace
8581 // N (ON): Other Neutrals
8582
8583 // Returns null if characters are ordered as they appear
8584 // (left-to-right), or an array of sections ({from, to, level}
8585 // objects) in the order in which they occur visually.
8586 var bidiOrdering = (function() {
8587 // Character types for codepoints 0 to 0xff
8588 var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN";
8589 // Character types for codepoints 0x600 to 0x6ff
8590 var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm";
8591 function charType(code) {
8592 if (code <= 0xf7) return lowTypes.charAt(code);
8593 else if (0x590 <= code && code <= 0x5f4) return "R";
8594 else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600);
8595 else if (0x6ee <= code && code <= 0x8ac) return "r";
8596 else if (0x2000 <= code && code <= 0x200b) return "w";
8597 else if (code == 0x200c) return "b";
8598 else return "L";
8599 }
8600
8601 var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
8602 var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
8603 // Browsers seem to always treat the boundaries of block elements as being L.
8604 var outerType = "L";
8605
8606 function BidiSpan(level, from, to) {
8607 this.level = level;
8608 this.from = from; this.to = to;
8609 }
8610
8611 return function(str) {
8612 if (!bidiRE.test(str)) return false;
8613 var len = str.length, types = [];
8614 for (var i = 0, type; i < len; ++i)
8615 types.push(type = charType(str.charCodeAt(i)));
8616
8617 // W1. Examine each non-spacing mark (NSM) in the level run, and
8618 // change the type of the NSM to the type of the previous
8619 // character. If the NSM is at the start of the level run, it will
8620 // get the type of sor.
8621 for (var i = 0, prev = outerType; i < len; ++i) {
8622 var type = types[i];
8623 if (type == "m") types[i] = prev;
8624 else prev = type;
8625 }
8626
8627 // W2. Search backwards from each instance of a European number
8628 // until the first strong type (R, L, AL, or sor) is found. If an
8629 // AL is found, change the type of the European number to Arabic
8630 // number.
8631 // W3. Change all ALs to R.
8632 for (var i = 0, cur = outerType; i < len; ++i) {
8633 var type = types[i];
8634 if (type == "1" && cur == "r") types[i] = "n";
8635 else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
8636 }
8637
8638 // W4. A single European separator between two European numbers
8639 // changes to a European number. A single common separator between
8640 // two numbers of the same type changes to that type.
8641 for (var i = 1, prev = types[0]; i < len - 1; ++i) {
8642 var type = types[i];
8643 if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
8644 else if (type == "," && prev == types[i+1] &&
8645 (prev == "1" || prev == "n")) types[i] = prev;
8646 prev = type;
8647 }
8648
8649 // W5. A sequence of European terminators adjacent to European
8650 // numbers changes to all European numbers.
8651 // W6. Otherwise, separators and terminators change to Other
8652 // Neutral.
8653 for (var i = 0; i < len; ++i) {
8654 var type = types[i];
8655 if (type == ",") types[i] = "N";
8656 else if (type == "%") {
8657 for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
8658 var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
8659 for (var j = i; j < end; ++j) types[j] = replace;
8660 i = end - 1;
8661 }
8662 }
8663
8664 // W7. Search backwards from each instance of a European number
8665 // until the first strong type (R, L, or sor) is found. If an L is
8666 // found, then change the type of the European number to L.
8667 for (var i = 0, cur = outerType; i < len; ++i) {
8668 var type = types[i];
8669 if (cur == "L" && type == "1") types[i] = "L";
8670 else if (isStrong.test(type)) cur = type;
8671 }
8672
8673 // N1. A sequence of neutrals takes the direction of the
8674 // surrounding strong text if the text on both sides has the same
8675 // direction. European and Arabic numbers act as if they were R in
8676 // terms of their influence on neutrals. Start-of-level-run (sor)
8677 // and end-of-level-run (eor) are used at level run boundaries.
8678 // N2. Any remaining neutrals take the embedding direction.
8679 for (var i = 0; i < len; ++i) {
8680 if (isNeutral.test(types[i])) {
8681 for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
8682 var before = (i ? types[i-1] : outerType) == "L";
8683 var after = (end < len ? types[end] : outerType) == "L";
8684 var replace = before || after ? "L" : "R";
8685 for (var j = i; j < end; ++j) types[j] = replace;
8686 i = end - 1;
8687 }
8688 }
8689
8690 // Here we depart from the documented algorithm, in order to avoid
8691 // building up an actual levels array. Since there are only three
8692 // levels (0, 1, 2) in an implementation that doesn't take
8693 // explicit embedding into account, we can build up the order on
8694 // the fly, without following the level-based algorithm.
8695 var order = [], m;
8696 for (var i = 0; i < len;) {
8697 if (countsAsLeft.test(types[i])) {
8698 var start = i;
8699 for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
8700 order.push(new BidiSpan(0, start, i));
8701 } else {
8702 var pos = i, at = order.length;
8703 for (++i; i < len && types[i] != "L"; ++i) {}
8704 for (var j = pos; j < i;) {
8705 if (countsAsNum.test(types[j])) {
8706 if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j));
8707 var nstart = j;
8708 for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
8709 order.splice(at, 0, new BidiSpan(2, nstart, j));
8710 pos = j;
8711 } else ++j;
8712 }
8713 if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i));
8714 }
8715 }
8716 if (order[0].level == 1 && (m = str.match(/^\s+/))) {
8717 order[0].from = m[0].length;
8718 order.unshift(new BidiSpan(0, 0, m[0].length));
8719 }
8720 if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
8721 lst(order).to -= m[0].length;
8722 order.push(new BidiSpan(0, len - m[0].length, len));
8723 }
8724 if (order[0].level == 2)
8725 order.unshift(new BidiSpan(1, order[0].to, order[0].to));
8726 if (order[0].level != lst(order).level)
8727 order.push(new BidiSpan(order[0].level, len, len));
8728
8729 return order;
8730 };
8731 })();
8732
8733 // THE END
8734
8735 CodeMirror.version = "5.2.0";
8736
8737 return CodeMirror;
8738});
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/javascript.js b/sources/samples/toolbarconfigurator/lib/codemirror/javascript.js
new file mode 100644
index 00000000..ef018478
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/javascript.js
@@ -0,0 +1,701 @@
1// CodeMirror, copyright (c) by Marijn Haverbeke and others
2// Distributed under an MIT license: http://codemirror.net/LICENSE
3
4// TODO actually recognize syntax of TypeScript constructs
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"use strict";
15
16CodeMirror.defineMode("javascript", function(config, parserConfig) {
17 var indentUnit = config.indentUnit;
18 var statementIndent = parserConfig.statementIndent;
19 var jsonldMode = parserConfig.jsonld;
20 var jsonMode = parserConfig.json || jsonldMode;
21 var isTS = parserConfig.typescript;
22 var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
23
24 // Tokenizer
25
26 var keywords = function(){
27 function kw(type) {return {type: type, style: "keyword"};}
28 var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
29 var operator = kw("operator"), atom = {type: "atom", style: "atom"};
30
31 var jsKeywords = {
32 "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
33 "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C,
34 "var": kw("var"), "const": kw("var"), "let": kw("var"),
35 "function": kw("function"), "catch": kw("catch"),
36 "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
37 "in": operator, "typeof": operator, "instanceof": operator,
38 "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
39 "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"),
40 "yield": C, "export": kw("export"), "import": kw("import"), "extends": C
41 };
42
43 // Extend the 'normal' keywords with the TypeScript language extensions
44 if (isTS) {
45 var type = {type: "variable", style: "variable-3"};
46 var tsKeywords = {
47 // object-like things
48 "interface": kw("interface"),
49 "extends": kw("extends"),
50 "constructor": kw("constructor"),
51
52 // scope modifiers
53 "public": kw("public"),
54 "private": kw("private"),
55 "protected": kw("protected"),
56 "static": kw("static"),
57
58 // types
59 "string": type, "number": type, "bool": type, "any": type
60 };
61
62 for (var attr in tsKeywords) {
63 jsKeywords[attr] = tsKeywords[attr];
64 }
65 }
66
67 return jsKeywords;
68 }();
69
70 var isOperatorChar = /[+\-*&%=<>!?|~^]/;
71 var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
72
73 function readRegexp(stream) {
74 var escaped = false, next, inSet = false;
75 while ((next = stream.next()) != null) {
76 if (!escaped) {
77 if (next == "/" && !inSet) return;
78 if (next == "[") inSet = true;
79 else if (inSet && next == "]") inSet = false;
80 }
81 escaped = !escaped && next == "\\";
82 }
83 }
84
85 // Used as scratch variables to communicate multiple values without
86 // consing up tons of objects.
87 var type, content;
88 function ret(tp, style, cont) {
89 type = tp; content = cont;
90 return style;
91 }
92 function tokenBase(stream, state) {
93 var ch = stream.next();
94 if (ch == '"' || ch == "'") {
95 state.tokenize = tokenString(ch);
96 return state.tokenize(stream, state);
97 } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
98 return ret("number", "number");
99 } else if (ch == "." && stream.match("..")) {
100 return ret("spread", "meta");
101 } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
102 return ret(ch);
103 } else if (ch == "=" && stream.eat(">")) {
104 return ret("=>", "operator");
105 } else if (ch == "0" && stream.eat(/x/i)) {
106 stream.eatWhile(/[\da-f]/i);
107 return ret("number", "number");
108 } else if (/\d/.test(ch)) {
109 stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
110 return ret("number", "number");
111 } else if (ch == "/") {
112 if (stream.eat("*")) {
113 state.tokenize = tokenComment;
114 return tokenComment(stream, state);
115 } else if (stream.eat("/")) {
116 stream.skipToEnd();
117 return ret("comment", "comment");
118 } else if (state.lastType == "operator" || state.lastType == "keyword c" ||
119 state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) {
120 readRegexp(stream);
121 stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
122 return ret("regexp", "string-2");
123 } else {
124 stream.eatWhile(isOperatorChar);
125 return ret("operator", "operator", stream.current());
126 }
127 } else if (ch == "`") {
128 state.tokenize = tokenQuasi;
129 return tokenQuasi(stream, state);
130 } else if (ch == "#") {
131 stream.skipToEnd();
132 return ret("error", "error");
133 } else if (isOperatorChar.test(ch)) {
134 stream.eatWhile(isOperatorChar);
135 return ret("operator", "operator", stream.current());
136 } else if (wordRE.test(ch)) {
137 stream.eatWhile(wordRE);
138 var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
139 return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
140 ret("variable", "variable", word);
141 }
142 }
143
144 function tokenString(quote) {
145 return function(stream, state) {
146 var escaped = false, next;
147 if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
148 state.tokenize = tokenBase;
149 return ret("jsonld-keyword", "meta");
150 }
151 while ((next = stream.next()) != null) {
152 if (next == quote && !escaped) break;
153 escaped = !escaped && next == "\\";
154 }
155 if (!escaped) state.tokenize = tokenBase;
156 return ret("string", "string");
157 };
158 }
159
160 function tokenComment(stream, state) {
161 var maybeEnd = false, ch;
162 while (ch = stream.next()) {
163 if (ch == "/" && maybeEnd) {
164 state.tokenize = tokenBase;
165 break;
166 }
167 maybeEnd = (ch == "*");
168 }
169 return ret("comment", "comment");
170 }
171
172 function tokenQuasi(stream, state) {
173 var escaped = false, next;
174 while ((next = stream.next()) != null) {
175 if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
176 state.tokenize = tokenBase;
177 break;
178 }
179 escaped = !escaped && next == "\\";
180 }
181 return ret("quasi", "string-2", stream.current());
182 }
183
184 var brackets = "([{}])";
185 // This is a crude lookahead trick to try and notice that we're
186 // parsing the argument patterns for a fat-arrow function before we
187 // actually hit the arrow token. It only works if the arrow is on
188 // the same line as the arguments and there's no strange noise
189 // (comments) in between. Fallback is to only notice when we hit the
190 // arrow, and not declare the arguments as locals for the arrow
191 // body.
192 function findFatArrow(stream, state) {
193 if (state.fatArrowAt) state.fatArrowAt = null;
194 var arrow = stream.string.indexOf("=>", stream.start);
195 if (arrow < 0) return;
196
197 var depth = 0, sawSomething = false;
198 for (var pos = arrow - 1; pos >= 0; --pos) {
199 var ch = stream.string.charAt(pos);
200 var bracket = brackets.indexOf(ch);
201 if (bracket >= 0 && bracket < 3) {
202 if (!depth) { ++pos; break; }
203 if (--depth == 0) break;
204 } else if (bracket >= 3 && bracket < 6) {
205 ++depth;
206 } else if (wordRE.test(ch)) {
207 sawSomething = true;
208 } else if (/["'\/]/.test(ch)) {
209 return;
210 } else if (sawSomething && !depth) {
211 ++pos;
212 break;
213 }
214 }
215 if (sawSomething && !depth) state.fatArrowAt = pos;
216 }
217
218 // Parser
219
220 var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
221
222 function JSLexical(indented, column, type, align, prev, info) {
223 this.indented = indented;
224 this.column = column;
225 this.type = type;
226 this.prev = prev;
227 this.info = info;
228 if (align != null) this.align = align;
229 }
230
231 function inScope(state, varname) {
232 for (var v = state.localVars; v; v = v.next)
233 if (v.name == varname) return true;
234 for (var cx = state.context; cx; cx = cx.prev) {
235 for (var v = cx.vars; v; v = v.next)
236 if (v.name == varname) return true;
237 }
238 }
239
240 function parseJS(state, style, type, content, stream) {
241 var cc = state.cc;
242 // Communicate our context to the combinators.
243 // (Less wasteful than consing up a hundred closures on every call.)
244 cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
245
246 if (!state.lexical.hasOwnProperty("align"))
247 state.lexical.align = true;
248
249 while(true) {
250 var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
251 if (combinator(type, content)) {
252 while(cc.length && cc[cc.length - 1].lex)
253 cc.pop()();
254 if (cx.marked) return cx.marked;
255 if (type == "variable" && inScope(state, content)) return "variable-2";
256 return style;
257 }
258 }
259 }
260
261 // Combinator utils
262
263 var cx = {state: null, column: null, marked: null, cc: null};
264 function pass() {
265 for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
266 }
267 function cont() {
268 pass.apply(null, arguments);
269 return true;
270 }
271 function register(varname) {
272 function inList(list) {
273 for (var v = list; v; v = v.next)
274 if (v.name == varname) return true;
275 return false;
276 }
277 var state = cx.state;
278 if (state.context) {
279 cx.marked = "def";
280 if (inList(state.localVars)) return;
281 state.localVars = {name: varname, next: state.localVars};
282 } else {
283 if (inList(state.globalVars)) return;
284 if (parserConfig.globalVars)
285 state.globalVars = {name: varname, next: state.globalVars};
286 }
287 }
288
289 // Combinators
290
291 var defaultVars = {name: "this", next: {name: "arguments"}};
292 function pushcontext() {
293 cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
294 cx.state.localVars = defaultVars;
295 }
296 function popcontext() {
297 cx.state.localVars = cx.state.context.vars;
298 cx.state.context = cx.state.context.prev;
299 }
300 function pushlex(type, info) {
301 var result = function() {
302 var state = cx.state, indent = state.indented;
303 if (state.lexical.type == "stat") indent = state.lexical.indented;
304 else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
305 indent = outer.indented;
306 state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
307 };
308 result.lex = true;
309 return result;
310 }
311 function poplex() {
312 var state = cx.state;
313 if (state.lexical.prev) {
314 if (state.lexical.type == ")")
315 state.indented = state.lexical.indented;
316 state.lexical = state.lexical.prev;
317 }
318 }
319 poplex.lex = true;
320
321 function expect(wanted) {
322 function exp(type) {
323 if (type == wanted) return cont();
324 else if (wanted == ";") return pass();
325 else return cont(exp);
326 };
327 return exp;
328 }
329
330 function statement(type, value) {
331 if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
332 if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
333 if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
334 if (type == "{") return cont(pushlex("}"), block, poplex);
335 if (type == ";") return cont();
336 if (type == "if") {
337 if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
338 cx.state.cc.pop()();
339 return cont(pushlex("form"), expression, statement, poplex, maybeelse);
340 }
341 if (type == "function") return cont(functiondef);
342 if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
343 if (type == "variable") return cont(pushlex("stat"), maybelabel);
344 if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
345 block, poplex, poplex);
346 if (type == "case") return cont(expression, expect(":"));
347 if (type == "default") return cont(expect(":"));
348 if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
349 statement, poplex, popcontext);
350 if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex);
351 if (type == "class") return cont(pushlex("form"), className, poplex);
352 if (type == "export") return cont(pushlex("form"), afterExport, poplex);
353 if (type == "import") return cont(pushlex("form"), afterImport, poplex);
354 return pass(pushlex("stat"), expression, expect(";"), poplex);
355 }
356 function expression(type) {
357 return expressionInner(type, false);
358 }
359 function expressionNoComma(type) {
360 return expressionInner(type, true);
361 }
362 function expressionInner(type, noComma) {
363 if (cx.state.fatArrowAt == cx.stream.start) {
364 var body = noComma ? arrowBodyNoComma : arrowBody;
365 if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
366 else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
367 }
368
369 var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
370 if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
371 if (type == "function") return cont(functiondef, maybeop);
372 if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
373 if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
374 if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
375 if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
376 if (type == "{") return contCommasep(objprop, "}", null, maybeop);
377 if (type == "quasi") { return pass(quasi, maybeop); }
378 return cont();
379 }
380 function maybeexpression(type) {
381 if (type.match(/[;\}\)\],]/)) return pass();
382 return pass(expression);
383 }
384 function maybeexpressionNoComma(type) {
385 if (type.match(/[;\}\)\],]/)) return pass();
386 return pass(expressionNoComma);
387 }
388
389 function maybeoperatorComma(type, value) {
390 if (type == ",") return cont(expression);
391 return maybeoperatorNoComma(type, value, false);
392 }
393 function maybeoperatorNoComma(type, value, noComma) {
394 var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
395 var expr = noComma == false ? expression : expressionNoComma;
396 if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
397 if (type == "operator") {
398 if (/\+\+|--/.test(value)) return cont(me);
399 if (value == "?") return cont(expression, expect(":"), expr);
400 return cont(expr);
401 }
402 if (type == "quasi") { return pass(quasi, me); }
403 if (type == ";") return;
404 if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
405 if (type == ".") return cont(property, me);
406 if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
407 }
408 function quasi(type, value) {
409 if (type != "quasi") return pass();
410 if (value.slice(value.length - 2) != "${") return cont(quasi);
411 return cont(expression, continueQuasi);
412 }
413 function continueQuasi(type) {
414 if (type == "}") {
415 cx.marked = "string-2";
416 cx.state.tokenize = tokenQuasi;
417 return cont(quasi);
418 }
419 }
420 function arrowBody(type) {
421 findFatArrow(cx.stream, cx.state);
422 return pass(type == "{" ? statement : expression);
423 }
424 function arrowBodyNoComma(type) {
425 findFatArrow(cx.stream, cx.state);
426 return pass(type == "{" ? statement : expressionNoComma);
427 }
428 function maybelabel(type) {
429 if (type == ":") return cont(poplex, statement);
430 return pass(maybeoperatorComma, expect(";"), poplex);
431 }
432 function property(type) {
433 if (type == "variable") {cx.marked = "property"; return cont();}
434 }
435 function objprop(type, value) {
436 if (type == "variable" || cx.style == "keyword") {
437 cx.marked = "property";
438 if (value == "get" || value == "set") return cont(getterSetter);
439 return cont(afterprop);
440 } else if (type == "number" || type == "string") {
441 cx.marked = jsonldMode ? "property" : (cx.style + " property");
442 return cont(afterprop);
443 } else if (type == "jsonld-keyword") {
444 return cont(afterprop);
445 } else if (type == "[") {
446 return cont(expression, expect("]"), afterprop);
447 }
448 }
449 function getterSetter(type) {
450 if (type != "variable") return pass(afterprop);
451 cx.marked = "property";
452 return cont(functiondef);
453 }
454 function afterprop(type) {
455 if (type == ":") return cont(expressionNoComma);
456 if (type == "(") return pass(functiondef);
457 }
458 function commasep(what, end) {
459 function proceed(type) {
460 if (type == ",") {
461 var lex = cx.state.lexical;
462 if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
463 return cont(what, proceed);
464 }
465 if (type == end) return cont();
466 return cont(expect(end));
467 }
468 return function(type) {
469 if (type == end) return cont();
470 return pass(what, proceed);
471 };
472 }
473 function contCommasep(what, end, info) {
474 for (var i = 3; i < arguments.length; i++)
475 cx.cc.push(arguments[i]);
476 return cont(pushlex(end, info), commasep(what, end), poplex);
477 }
478 function block(type) {
479 if (type == "}") return cont();
480 return pass(statement, block);
481 }
482 function maybetype(type) {
483 if (isTS && type == ":") return cont(typedef);
484 }
485 function typedef(type) {
486 if (type == "variable"){cx.marked = "variable-3"; return cont();}
487 }
488 function vardef() {
489 return pass(pattern, maybetype, maybeAssign, vardefCont);
490 }
491 function pattern(type, value) {
492 if (type == "variable") { register(value); return cont(); }
493 if (type == "[") return contCommasep(pattern, "]");
494 if (type == "{") return contCommasep(proppattern, "}");
495 }
496 function proppattern(type, value) {
497 if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
498 register(value);
499 return cont(maybeAssign);
500 }
501 if (type == "variable") cx.marked = "property";
502 return cont(expect(":"), pattern, maybeAssign);
503 }
504 function maybeAssign(_type, value) {
505 if (value == "=") return cont(expressionNoComma);
506 }
507 function vardefCont(type) {
508 if (type == ",") return cont(vardef);
509 }
510 function maybeelse(type, value) {
511 if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
512 }
513 function forspec(type) {
514 if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
515 }
516 function forspec1(type) {
517 if (type == "var") return cont(vardef, expect(";"), forspec2);
518 if (type == ";") return cont(forspec2);
519 if (type == "variable") return cont(formaybeinof);
520 return pass(expression, expect(";"), forspec2);
521 }
522 function formaybeinof(_type, value) {
523 if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
524 return cont(maybeoperatorComma, forspec2);
525 }
526 function forspec2(type, value) {
527 if (type == ";") return cont(forspec3);
528 if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
529 return pass(expression, expect(";"), forspec3);
530 }
531 function forspec3(type) {
532 if (type != ")") cont(expression);
533 }
534 function functiondef(type, value) {
535 if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
536 if (type == "variable") {register(value); return cont(functiondef);}
537 if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext);
538 }
539 function funarg(type) {
540 if (type == "spread") return cont(funarg);
541 return pass(pattern, maybetype);
542 }
543 function className(type, value) {
544 if (type == "variable") {register(value); return cont(classNameAfter);}
545 }
546 function classNameAfter(type, value) {
547 if (value == "extends") return cont(expression, classNameAfter);
548 if (type == "{") return cont(pushlex("}"), classBody, poplex);
549 }
550 function classBody(type, value) {
551 if (type == "variable" || cx.style == "keyword") {
552 if (value == "static") {
553 cx.marked = "keyword";
554 return cont(classBody);
555 }
556 cx.marked = "property";
557 if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
558 return cont(functiondef, classBody);
559 }
560 if (value == "*") {
561 cx.marked = "keyword";
562 return cont(classBody);
563 }
564 if (type == ";") return cont(classBody);
565 if (type == "}") return cont();
566 }
567 function classGetterSetter(type) {
568 if (type != "variable") return pass();
569 cx.marked = "property";
570 return cont();
571 }
572 function afterModule(type, value) {
573 if (type == "string") return cont(statement);
574 if (type == "variable") { register(value); return cont(maybeFrom); }
575 }
576 function afterExport(_type, value) {
577 if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
578 if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
579 return pass(statement);
580 }
581 function afterImport(type) {
582 if (type == "string") return cont();
583 return pass(importSpec, maybeFrom);
584 }
585 function importSpec(type, value) {
586 if (type == "{") return contCommasep(importSpec, "}");
587 if (type == "variable") register(value);
588 if (value == "*") cx.marked = "keyword";
589 return cont(maybeAs);
590 }
591 function maybeAs(_type, value) {
592 if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
593 }
594 function maybeFrom(_type, value) {
595 if (value == "from") { cx.marked = "keyword"; return cont(expression); }
596 }
597 function arrayLiteral(type) {
598 if (type == "]") return cont();
599 return pass(expressionNoComma, maybeArrayComprehension);
600 }
601 function maybeArrayComprehension(type) {
602 if (type == "for") return pass(comprehension, expect("]"));
603 if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
604 return pass(commasep(expressionNoComma, "]"));
605 }
606 function comprehension(type) {
607 if (type == "for") return cont(forspec, comprehension);
608 if (type == "if") return cont(expression, comprehension);
609 }
610
611 function isContinuedStatement(state, textAfter) {
612 return state.lastType == "operator" || state.lastType == "," ||
613 isOperatorChar.test(textAfter.charAt(0)) ||
614 /[,.]/.test(textAfter.charAt(0));
615 }
616
617 // Interface
618
619 return {
620 startState: function(basecolumn) {
621 var state = {
622 tokenize: tokenBase,
623 lastType: "sof",
624 cc: [],
625 lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
626 localVars: parserConfig.localVars,
627 context: parserConfig.localVars && {vars: parserConfig.localVars},
628 indented: 0
629 };
630 if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
631 state.globalVars = parserConfig.globalVars;
632 return state;
633 },
634
635 token: function(stream, state) {
636 if (stream.sol()) {
637 if (!state.lexical.hasOwnProperty("align"))
638 state.lexical.align = false;
639 state.indented = stream.indentation();
640 findFatArrow(stream, state);
641 }
642 if (state.tokenize != tokenComment && stream.eatSpace()) return null;
643 var style = state.tokenize(stream, state);
644 if (type == "comment") return style;
645 state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
646 return parseJS(state, style, type, content, stream);
647 },
648
649 indent: function(state, textAfter) {
650 if (state.tokenize == tokenComment) return CodeMirror.Pass;
651 if (state.tokenize != tokenBase) return 0;
652 var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
653 // Kludge to prevent 'maybelse' from blocking lexical scope pops
654 if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
655 var c = state.cc[i];
656 if (c == poplex) lexical = lexical.prev;
657 else if (c != maybeelse) break;
658 }
659 if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
660 if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
661 lexical = lexical.prev;
662 var type = lexical.type, closing = firstChar == type;
663
664 if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
665 else if (type == "form" && firstChar == "{") return lexical.indented;
666 else if (type == "form") return lexical.indented + indentUnit;
667 else if (type == "stat")
668 return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
669 else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
670 return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
671 else if (lexical.align) return lexical.column + (closing ? 0 : 1);
672 else return lexical.indented + (closing ? 0 : indentUnit);
673 },
674
675 electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
676 blockCommentStart: jsonMode ? null : "/*",
677 blockCommentEnd: jsonMode ? null : "*/",
678 lineComment: jsonMode ? null : "//",
679 fold: "brace",
680 closeBrackets: "()[]{}''\"\"``",
681
682 helperType: jsonMode ? "json" : "javascript",
683 jsonldMode: jsonldMode,
684 jsonMode: jsonMode
685 };
686});
687
688CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
689
690CodeMirror.defineMIME("text/javascript", "javascript");
691CodeMirror.defineMIME("text/ecmascript", "javascript");
692CodeMirror.defineMIME("application/javascript", "javascript");
693CodeMirror.defineMIME("application/x-javascript", "javascript");
694CodeMirror.defineMIME("application/ecmascript", "javascript");
695CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
696CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
697CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
698CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
699CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
700
701});
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/neo.css b/sources/samples/toolbarconfigurator/lib/codemirror/neo.css
new file mode 100644
index 00000000..d019aab8
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/neo.css
@@ -0,0 +1,36 @@
1/* neo theme for codemirror */
2
3/* Color scheme */
4
5.cm-s-neo.CodeMirror {
6 background-color:#ffffff;
7 color:#2e383c;
8 line-height:1.4375;
9}
10.cm-s-neo .cm-comment {color:#75787b}
11.cm-s-neo .cm-keyword, .cm-s-neo .cm-property {color:#1d75b3}
12.cm-s-neo .cm-atom,.cm-s-neo .cm-number {color:#75438a}
13.cm-s-neo .cm-node,.cm-s-neo .cm-tag {color:#9c3328}
14.cm-s-neo .cm-string {color:#b35e14}
15.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier {color:#047d65}
16
17
18/* Editor styling */
19
20.cm-s-neo pre {
21 padding:0;
22}
23
24.cm-s-neo .CodeMirror-gutters {
25 border:none;
26 border-right:10px solid transparent;
27 background-color:transparent;
28}
29
30.cm-s-neo .CodeMirror-linenumber {
31 padding:0;
32 color:#e0e2e5;
33}
34
35.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }
36.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.css b/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.css
new file mode 100644
index 00000000..924e638f
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.css
@@ -0,0 +1,38 @@
1.CodeMirror-hints {
2 position: absolute;
3 z-index: 10;
4 overflow: hidden;
5 list-style: none;
6
7 margin: 0;
8 padding: 2px;
9
10 -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
11 -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
12 box-shadow: 2px 3px 5px rgba(0,0,0,.2);
13 border-radius: 3px;
14 border: 1px solid silver;
15
16 background: white;
17 font-size: 90%;
18 font-family: monospace;
19
20 max-height: 20em;
21 overflow-y: auto;
22}
23
24.CodeMirror-hint {
25 margin: 0;
26 padding: 0 4px;
27 border-radius: 2px;
28 max-width: 19em;
29 overflow: hidden;
30 white-space: pre;
31 color: black;
32 cursor: pointer;
33}
34
35li.CodeMirror-hint-active {
36 background: #08f;
37 color: white;
38}
diff --git a/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.js b/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.js
new file mode 100644
index 00000000..539181fd
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/lib/codemirror/show-hint.js
@@ -0,0 +1,392 @@
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 var HINT_ELEMENT_CLASS = "CodeMirror-hint";
15 var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
16
17 // This is the old interface, kept around for now to stay
18 // backwards-compatible.
19 CodeMirror.showHint = function(cm, getHints, options) {
20 if (!getHints) return cm.showHint(options);
21 if (options && options.async) getHints.async = true;
22 var newOpts = {hint: getHints};
23 if (options) for (var prop in options) newOpts[prop] = options[prop];
24 return cm.showHint(newOpts);
25 };
26
27 CodeMirror.defineExtension("showHint", function(options) {
28 // We want a single cursor position.
29 if (this.listSelections().length > 1 || this.somethingSelected()) return;
30
31 if (this.state.completionActive) this.state.completionActive.close();
32 var completion = this.state.completionActive = new Completion(this, options);
33 if (!completion.options.hint) return;
34
35 CodeMirror.signal(this, "startCompletion", this);
36 completion.update();
37 });
38
39 function Completion(cm, options) {
40 this.cm = cm;
41 this.options = this.buildOptions(options);
42 this.widget = null;
43 this.debounce = 0;
44 this.tick = 0;
45 this.startPos = this.cm.getCursor();
46 this.startLen = this.cm.getLine(this.startPos.line).length;
47
48 var self = this;
49 cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
50 }
51
52 var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
53 return setTimeout(fn, 1000/60);
54 };
55 var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
56
57 Completion.prototype = {
58 close: function() {
59 if (!this.active()) return;
60 this.cm.state.completionActive = null;
61 this.tick = null;
62 this.cm.off("cursorActivity", this.activityFunc);
63
64 if (this.widget) this.widget.close();
65 CodeMirror.signal(this.cm, "endCompletion", this.cm);
66 },
67
68 active: function() {
69 return this.cm.state.completionActive == this;
70 },
71
72 pick: function(data, i) {
73 var completion = data.list[i];
74 if (completion.hint) completion.hint(this.cm, data, completion);
75 else this.cm.replaceRange(getText(completion), completion.from || data.from,
76 completion.to || data.to, "complete");
77 CodeMirror.signal(data, "pick", completion);
78 this.close();
79 },
80
81 showHints: function(data) {
82 if (!data || !data.list.length || !this.active()) return this.close();
83
84 if (this.options.completeSingle && data.list.length == 1)
85 this.pick(data, 0);
86 else
87 this.showWidget(data);
88 },
89
90 cursorActivity: function() {
91 if (this.debounce) {
92 cancelAnimationFrame(this.debounce);
93 this.debounce = 0;
94 }
95
96 var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
97 if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
98 pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
99 (pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
100 this.close();
101 } else {
102 var self = this;
103 this.debounce = requestAnimationFrame(function() {self.update();});
104 if (this.widget) this.widget.disable();
105 }
106 },
107
108 update: function() {
109 if (this.tick == null) return;
110 if (this.data) CodeMirror.signal(this.data, "update");
111 if (!this.options.hint.async) {
112 this.finishUpdate(this.options.hint(this.cm, this.options), myTick);
113 } else {
114 var myTick = ++this.tick, self = this;
115 this.options.hint(this.cm, function(data) {
116 if (self.tick == myTick) self.finishUpdate(data);
117 }, this.options);
118 }
119 },
120
121 finishUpdate: function(data) {
122 this.data = data;
123 var picked = this.widget && this.widget.picked;
124 if (this.widget) this.widget.close();
125 if (data && data.list.length) {
126 if (picked && data.list.length == 1) this.pick(data, 0);
127 else this.widget = new Widget(this, data);
128 }
129 },
130
131 showWidget: function(data) {
132 this.data = data;
133 this.widget = new Widget(this, data);
134 CodeMirror.signal(data, "shown");
135 },
136
137 buildOptions: function(options) {
138 var editor = this.cm.options.hintOptions;
139 var out = {};
140 for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
141 if (editor) for (var prop in editor)
142 if (editor[prop] !== undefined) out[prop] = editor[prop];
143 if (options) for (var prop in options)
144 if (options[prop] !== undefined) out[prop] = options[prop];
145 return out;
146 }
147 };
148
149 function getText(completion) {
150 if (typeof completion == "string") return completion;
151 else return completion.text;
152 }
153
154 function buildKeyMap(completion, handle) {
155 var baseMap = {
156 Up: function() {handle.moveFocus(-1);},
157 Down: function() {handle.moveFocus(1);},
158 PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
159 PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
160 Home: function() {handle.setFocus(0);},
161 End: function() {handle.setFocus(handle.length - 1);},
162 Enter: handle.pick,
163 Tab: handle.pick,
164 Esc: handle.close
165 };
166 var custom = completion.options.customKeys;
167 var ourMap = custom ? {} : baseMap;
168 function addBinding(key, val) {
169 var bound;
170 if (typeof val != "string")
171 bound = function(cm) { return val(cm, handle); };
172 // This mechanism is deprecated
173 else if (baseMap.hasOwnProperty(val))
174 bound = baseMap[val];
175 else
176 bound = val;
177 ourMap[key] = bound;
178 }
179 if (custom)
180 for (var key in custom) if (custom.hasOwnProperty(key))
181 addBinding(key, custom[key]);
182 var extra = completion.options.extraKeys;
183 if (extra)
184 for (var key in extra) if (extra.hasOwnProperty(key))
185 addBinding(key, extra[key]);
186 return ourMap;
187 }
188
189 function getHintElement(hintsElement, el) {
190 while (el && el != hintsElement) {
191 if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
192 el = el.parentNode;
193 }
194 }
195
196 function Widget(completion, data) {
197 this.completion = completion;
198 this.data = data;
199 this.picked = false;
200 var widget = this, cm = completion.cm;
201
202 var hints = this.hints = document.createElement("ul");
203 hints.className = "CodeMirror-hints";
204 this.selectedHint = data.selectedHint || 0;
205
206 var completions = data.list;
207 for (var i = 0; i < completions.length; ++i) {
208 var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
209 var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
210 if (cur.className != null) className = cur.className + " " + className;
211 elt.className = className;
212 if (cur.render) cur.render(elt, data, cur);
213 else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
214 elt.hintId = i;
215 }
216
217 var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
218 var left = pos.left, top = pos.bottom, below = true;
219 hints.style.left = left + "px";
220 hints.style.top = top + "px";
221 // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
222 var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
223 var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
224 (completion.options.container || document.body).appendChild(hints);
225 var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
226 if (overlapY > 0) {
227 var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
228 if (curTop - height > 0) { // Fits above cursor
229 hints.style.top = (top = pos.top - height) + "px";
230 below = false;
231 } else if (height > winH) {
232 hints.style.height = (winH - 5) + "px";
233 hints.style.top = (top = pos.bottom - box.top) + "px";
234 var cursor = cm.getCursor();
235 if (data.from.ch != cursor.ch) {
236 pos = cm.cursorCoords(cursor);
237 hints.style.left = (left = pos.left) + "px";
238 box = hints.getBoundingClientRect();
239 }
240 }
241 }
242 var overlapX = box.right - winW;
243 if (overlapX > 0) {
244 if (box.right - box.left > winW) {
245 hints.style.width = (winW - 5) + "px";
246 overlapX -= (box.right - box.left) - winW;
247 }
248 hints.style.left = (left = pos.left - overlapX) + "px";
249 }
250
251 cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
252 moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
253 setFocus: function(n) { widget.changeActive(n); },
254 menuSize: function() { return widget.screenAmount(); },
255 length: completions.length,
256 close: function() { completion.close(); },
257 pick: function() { widget.pick(); },
258 data: data
259 }));
260
261 if (completion.options.closeOnUnfocus) {
262 var closingOnBlur;
263 cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
264 cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
265 }
266
267 var startScroll = cm.getScrollInfo();
268 cm.on("scroll", this.onScroll = function() {
269 var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
270 var newTop = top + startScroll.top - curScroll.top;
271 var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
272 if (!below) point += hints.offsetHeight;
273 if (point <= editor.top || point >= editor.bottom) return completion.close();
274 hints.style.top = newTop + "px";
275 hints.style.left = (left + startScroll.left - curScroll.left) + "px";
276 });
277
278 CodeMirror.on(hints, "dblclick", function(e) {
279 var t = getHintElement(hints, e.target || e.srcElement);
280 if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
281 });
282
283 CodeMirror.on(hints, "click", function(e) {
284 var t = getHintElement(hints, e.target || e.srcElement);
285 if (t && t.hintId != null) {
286 widget.changeActive(t.hintId);
287 if (completion.options.completeOnSingleClick) widget.pick();
288 }
289 });
290
291 CodeMirror.on(hints, "mousedown", function() {
292 setTimeout(function(){cm.focus();}, 20);
293 });
294
295 CodeMirror.signal(data, "select", completions[0], hints.firstChild);
296 return true;
297 }
298
299 Widget.prototype = {
300 close: function() {
301 if (this.completion.widget != this) return;
302 this.completion.widget = null;
303 this.hints.parentNode.removeChild(this.hints);
304 this.completion.cm.removeKeyMap(this.keyMap);
305
306 var cm = this.completion.cm;
307 if (this.completion.options.closeOnUnfocus) {
308 cm.off("blur", this.onBlur);
309 cm.off("focus", this.onFocus);
310 }
311 cm.off("scroll", this.onScroll);
312 },
313
314 disable: function() {
315 this.completion.cm.removeKeyMap(this.keyMap);
316 var widget = this;
317 this.keyMap = {Enter: function() { widget.picked = true; }};
318 this.completion.cm.addKeyMap(this.keyMap);
319 },
320
321 pick: function() {
322 this.completion.pick(this.data, this.selectedHint);
323 },
324
325 changeActive: function(i, avoidWrap) {
326 if (i >= this.data.list.length)
327 i = avoidWrap ? this.data.list.length - 1 : 0;
328 else if (i < 0)
329 i = avoidWrap ? 0 : this.data.list.length - 1;
330 if (this.selectedHint == i) return;
331 var node = this.hints.childNodes[this.selectedHint];
332 node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
333 node = this.hints.childNodes[this.selectedHint = i];
334 node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
335 if (node.offsetTop < this.hints.scrollTop)
336 this.hints.scrollTop = node.offsetTop - 3;
337 else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
338 this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
339 CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
340 },
341
342 screenAmount: function() {
343 return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
344 }
345 };
346
347 CodeMirror.registerHelper("hint", "auto", function(cm, options) {
348 var helpers = cm.getHelpers(cm.getCursor(), "hint"), words;
349 if (helpers.length) {
350 for (var i = 0; i < helpers.length; i++) {
351 var cur = helpers[i](cm, options);
352 if (cur && cur.list.length) return cur;
353 }
354 } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
355 if (words) return CodeMirror.hint.fromList(cm, {words: words});
356 } else if (CodeMirror.hint.anyword) {
357 return CodeMirror.hint.anyword(cm, options);
358 }
359 });
360
361 CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
362 var cur = cm.getCursor(), token = cm.getTokenAt(cur);
363 var found = [];
364 for (var i = 0; i < options.words.length; i++) {
365 var word = options.words[i];
366 if (word.slice(0, token.string.length) == token.string)
367 found.push(word);
368 }
369
370 if (found.length) return {
371 list: found,
372 from: CodeMirror.Pos(cur.line, token.start),
373 to: CodeMirror.Pos(cur.line, token.end)
374 };
375 });
376
377 CodeMirror.commands.autocomplete = CodeMirror.showHint;
378
379 var defaultOptions = {
380 hint: CodeMirror.hint.auto,
381 completeSingle: true,
382 alignWithWord: true,
383 closeCharacters: /[\s()\[\]{};:>,]/,
384 closeOnUnfocus: true,
385 completeOnSingleClick: false,
386 container: null,
387 customKeys: null,
388 extraKeys: null
389 };
390
391 CodeMirror.defineOption("hintOptions", null);
392});
diff --git a/sources/samples/toolbarconfigurator/package.json b/sources/samples/toolbarconfigurator/package.json
new file mode 100644
index 00000000..1fceff9c
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/package.json
@@ -0,0 +1,12 @@
1{
2 "name": "ckeditor-toolbarconfigurator",
3 "version": "0.0.0",
4 "description": "",
5 "author": "CKSource (http://cksource.com/)",
6 "license": "For licensing, see LICENSE.md or http://ckeditor.com/license.",
7 "devDependencies": {
8 "benderjs": "~0.2.1",
9 "benderjs-chai": "~0.2.0",
10 "benderjs-mocha": "~0.1.2"
11 }
12}
diff --git a/sources/samples/toolbarconfigurator/tests/one.js b/sources/samples/toolbarconfigurator/tests/one.js
new file mode 100644
index 00000000..1d3837f5
--- /dev/null
+++ b/sources/samples/toolbarconfigurator/tests/one.js
@@ -0,0 +1,9 @@
1/* global describe, it, expect, ToolbarConfigurator */
2
3describe( 'Full toolbar configurator', function() {
4 var FTE = ToolbarConfigurator.FullToolbarEditor;
5
6 it( 'exists', function() {
7 expect( FTE ).to.be.a( 'function' );
8 } );
9} );