|
1 /** |
|
2 * plugin.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /*global tinymce:true */ |
|
12 |
|
13 tinymce.PluginManager.add('textpattern', function(editor) { |
|
14 var isPatternsDirty = true, patterns; |
|
15 |
|
16 patterns = editor.settings.textpattern_patterns || [ |
|
17 {start: '*', end: '*', format: 'italic'}, |
|
18 {start: '**', end: '**', format: 'bold'}, |
|
19 {start: '#', format: 'h1'}, |
|
20 {start: '##', format: 'h2'}, |
|
21 {start: '###', format: 'h3'}, |
|
22 {start: '####', format: 'h4'}, |
|
23 {start: '#####', format: 'h5'}, |
|
24 {start: '######', format: 'h6'}, |
|
25 {start: '1. ', cmd: 'InsertOrderedList'}, |
|
26 {start: '* ', cmd: 'InsertUnorderedList'}, |
|
27 {start: '- ', cmd: 'InsertUnorderedList'} |
|
28 ]; |
|
29 |
|
30 // Returns a sorted patterns list, ordered descending by start length |
|
31 function getPatterns() { |
|
32 if (isPatternsDirty) { |
|
33 patterns.sort(function(a, b) { |
|
34 if (a.start.length > b.start.length) { |
|
35 return -1; |
|
36 } |
|
37 |
|
38 if (a.start.length < b.start.length) { |
|
39 return 1; |
|
40 } |
|
41 |
|
42 return 0; |
|
43 }); |
|
44 |
|
45 isPatternsDirty = false; |
|
46 } |
|
47 |
|
48 return patterns; |
|
49 } |
|
50 |
|
51 // Finds a matching pattern to the specified text |
|
52 function findPattern(text) { |
|
53 var patterns = getPatterns(); |
|
54 |
|
55 for (var i = 0; i < patterns.length; i++) { |
|
56 if (text.indexOf(patterns[i].start) !== 0) { |
|
57 continue; |
|
58 } |
|
59 |
|
60 if (patterns[i].end && text.lastIndexOf(patterns[i].end) != text.length - patterns[i].end.length) { |
|
61 continue; |
|
62 } |
|
63 |
|
64 return patterns[i]; |
|
65 } |
|
66 } |
|
67 |
|
68 // Finds the best matching end pattern |
|
69 function findEndPattern(text, offset, delta) { |
|
70 var patterns, pattern, i; |
|
71 |
|
72 // Find best matching end |
|
73 patterns = getPatterns(); |
|
74 for (i = 0; i < patterns.length; i++) { |
|
75 pattern = patterns[i]; |
|
76 if (pattern.end && text.substr(offset - pattern.end.length - delta, pattern.end.length) == pattern.end) { |
|
77 return pattern; |
|
78 } |
|
79 } |
|
80 } |
|
81 |
|
82 // Handles inline formats like *abc* and **abc** |
|
83 function applyInlineFormat(space) { |
|
84 var selection, dom, rng, container, offset, startOffset, text, patternRng, pattern, delta, format; |
|
85 |
|
86 function splitContainer() { |
|
87 // Split text node and remove start/end from text node |
|
88 container = container.splitText(startOffset); |
|
89 container.splitText(offset - startOffset - delta); |
|
90 container.deleteData(0, pattern.start.length); |
|
91 container.deleteData(container.data.length - pattern.end.length, pattern.end.length); |
|
92 } |
|
93 |
|
94 selection = editor.selection; |
|
95 dom = editor.dom; |
|
96 |
|
97 if (!selection.isCollapsed()) { |
|
98 return; |
|
99 } |
|
100 |
|
101 rng = selection.getRng(true); |
|
102 container = rng.startContainer; |
|
103 offset = rng.startOffset; |
|
104 text = container.data; |
|
105 delta = space ? 1 : 0; |
|
106 |
|
107 if (container.nodeType != 3) { |
|
108 return; |
|
109 } |
|
110 |
|
111 // Find best matching end |
|
112 pattern = findEndPattern(text, offset, delta); |
|
113 if (!pattern) { |
|
114 return; |
|
115 } |
|
116 |
|
117 // Find start of matched pattern |
|
118 // TODO: Might need to improve this if there is nested formats |
|
119 startOffset = Math.max(0, offset - delta); |
|
120 startOffset = text.lastIndexOf(pattern.start, startOffset - pattern.end.length - 1); |
|
121 |
|
122 if (startOffset === -1) { |
|
123 return; |
|
124 } |
|
125 |
|
126 // Setup a range for the matching word |
|
127 patternRng = dom.createRng(); |
|
128 patternRng.setStart(container, startOffset); |
|
129 patternRng.setEnd(container, offset - delta); |
|
130 pattern = findPattern(patternRng.toString()); |
|
131 |
|
132 if (!pattern || !pattern.end) { |
|
133 return; |
|
134 } |
|
135 |
|
136 // If container match doesn't have anything between start/end then do nothing |
|
137 if (container.data.length <= pattern.start.length + pattern.end.length) { |
|
138 return; |
|
139 } |
|
140 |
|
141 format = editor.formatter.get(pattern.format); |
|
142 if (format && format[0].inline) { |
|
143 splitContainer(); |
|
144 editor.formatter.apply(pattern.format, {}, container); |
|
145 return container; |
|
146 } |
|
147 } |
|
148 |
|
149 // Handles block formats like ##abc or 1. abc |
|
150 function applyBlockFormat() { |
|
151 var selection, dom, container, firstTextNode, node, format, textBlockElm, pattern, walker, rng, offset; |
|
152 |
|
153 selection = editor.selection; |
|
154 dom = editor.dom; |
|
155 |
|
156 if (!selection.isCollapsed()) { |
|
157 return; |
|
158 } |
|
159 |
|
160 textBlockElm = dom.getParent(selection.getStart(), 'p'); |
|
161 if (textBlockElm) { |
|
162 walker = new tinymce.dom.TreeWalker(textBlockElm, textBlockElm); |
|
163 while ((node = walker.next())) { |
|
164 if (node.nodeType == 3) { |
|
165 firstTextNode = node; |
|
166 break; |
|
167 } |
|
168 } |
|
169 |
|
170 if (firstTextNode) { |
|
171 pattern = findPattern(firstTextNode.data); |
|
172 if (!pattern) { |
|
173 return; |
|
174 } |
|
175 |
|
176 rng = selection.getRng(true); |
|
177 container = rng.startContainer; |
|
178 offset = rng.startOffset; |
|
179 |
|
180 if (firstTextNode == container) { |
|
181 offset = Math.max(0, offset - pattern.start.length); |
|
182 } |
|
183 |
|
184 if (tinymce.trim(firstTextNode.data).length == pattern.start.length) { |
|
185 return; |
|
186 } |
|
187 |
|
188 if (pattern.format) { |
|
189 format = editor.formatter.get(pattern.format); |
|
190 if (format && format[0].block) { |
|
191 firstTextNode.deleteData(0, pattern.start.length); |
|
192 editor.formatter.apply(pattern.format, {}, firstTextNode); |
|
193 |
|
194 rng.setStart(container, offset); |
|
195 rng.collapse(true); |
|
196 selection.setRng(rng); |
|
197 } |
|
198 } |
|
199 |
|
200 if (pattern.cmd) { |
|
201 editor.undoManager.transact(function() { |
|
202 firstTextNode.deleteData(0, pattern.start.length); |
|
203 editor.execCommand(pattern.cmd); |
|
204 }); |
|
205 } |
|
206 } |
|
207 } |
|
208 } |
|
209 |
|
210 function handleEnter() { |
|
211 var rng, wrappedTextNode; |
|
212 |
|
213 wrappedTextNode = applyInlineFormat(); |
|
214 if (wrappedTextNode) { |
|
215 rng = editor.dom.createRng(); |
|
216 rng.setStart(wrappedTextNode, wrappedTextNode.data.length); |
|
217 rng.setEnd(wrappedTextNode, wrappedTextNode.data.length); |
|
218 editor.selection.setRng(rng); |
|
219 } |
|
220 |
|
221 applyBlockFormat(); |
|
222 } |
|
223 |
|
224 function handleSpace() { |
|
225 var wrappedTextNode, lastChar, lastCharNode, rng, dom; |
|
226 |
|
227 wrappedTextNode = applyInlineFormat(true); |
|
228 if (wrappedTextNode) { |
|
229 dom = editor.dom; |
|
230 lastChar = wrappedTextNode.data.slice(-1); |
|
231 |
|
232 // Move space after the newly formatted node |
|
233 if (/[\u00a0 ]/.test(lastChar)) { |
|
234 wrappedTextNode.deleteData(wrappedTextNode.data.length - 1, 1); |
|
235 lastCharNode = dom.doc.createTextNode(lastChar); |
|
236 |
|
237 if (wrappedTextNode.nextSibling) { |
|
238 dom.insertAfter(lastCharNode, wrappedTextNode.nextSibling); |
|
239 } else { |
|
240 wrappedTextNode.parentNode.appendChild(lastCharNode); |
|
241 } |
|
242 |
|
243 rng = dom.createRng(); |
|
244 rng.setStart(lastCharNode, 1); |
|
245 rng.setEnd(lastCharNode, 1); |
|
246 editor.selection.setRng(rng); |
|
247 } |
|
248 } |
|
249 } |
|
250 |
|
251 editor.on('keydown', function(e) { |
|
252 if (e.keyCode == 13 && !tinymce.util.VK.modifierPressed(e)) { |
|
253 handleEnter(); |
|
254 } |
|
255 }, true); |
|
256 |
|
257 editor.on('keyup', function(e) { |
|
258 if (e.keyCode == 32 && !tinymce.util.VK.modifierPressed(e)) { |
|
259 handleSpace(); |
|
260 } |
|
261 }); |
|
262 |
|
263 this.getPatterns = getPatterns; |
|
264 this.setPatterns = function(newPatterns) { |
|
265 patterns = newPatterns; |
|
266 isPatternsDirty = true; |
|
267 }; |
|
268 }); |