|
1 CKEDITOR.plugins.add('crossreference', { |
|
2 lang : [ 'en', 'ru' ], |
|
3 requires : 'dialog,notification', |
|
4 icons : 'crossreference', |
|
5 hidpi : true, |
|
6 init : function(editor) { |
|
7 |
|
8 // config |
|
9 |
|
10 var config = getConfig(); |
|
11 editor.config.crossreference = config; |
|
12 |
|
13 // plugin |
|
14 |
|
15 var anchorAllowedContent = 'a[!cross-reference,cross-anchor,cross-guid,cross-name,cross-number]{*}(cross-reference,cross-anchor)'; |
|
16 var anchorRequiredContent = 'a[cross-reference,cross-anchor]'; |
|
17 var linkAllowedContent = 'a[!cross-reference,cross-link,cross-guid,cross-name,cross-number]{*}(cross-reference,cross-link)'; |
|
18 var linkRequiredContent = 'a[cross-reference,cross-link]'; |
|
19 editor.addFeature({ |
|
20 name: 'crossreference-anchor', |
|
21 allowedContent: anchorAllowedContent, |
|
22 requiredContent: anchorRequiredContent |
|
23 }); |
|
24 editor.addFeature({ |
|
25 name: 'crossreference-link', |
|
26 allowedContent: linkAllowedContent, |
|
27 requiredContent: linkRequiredContent |
|
28 }); |
|
29 editor.ui.add('crossreference', CKEDITOR.UI_MENUBUTTON, { |
|
30 label : editor.lang.crossreference.name, |
|
31 modes: { |
|
32 wysiwyg: 1, |
|
33 source: 1 |
|
34 }, |
|
35 toolbar : 'insert', |
|
36 onMenu: function() { |
|
37 var selectedElement = null; |
|
38 |
|
39 var selection = editor.getSelection(); |
|
40 if (selection) { |
|
41 var element = selection.getStartElement(); |
|
42 if (element) |
|
43 element = element.getAscendant('a', true); |
|
44 if (element && element.hasAttribute('cross-reference')) { |
|
45 selectedElement = element; |
|
46 } |
|
47 } |
|
48 |
|
49 var state = getMenuState(selectedElement, true); |
|
50 return state; |
|
51 } |
|
52 }); |
|
53 |
|
54 // dialogs |
|
55 |
|
56 var updateCmdName = 'update-crossreferences'; |
|
57 var anchorDialogCmdName = 'crossreference-anchor-dialog'; |
|
58 var linkDialogCmdName = 'crossreference-link-dialog'; |
|
59 |
|
60 CKEDITOR.dialog.add(anchorDialogCmdName, this.path + 'dialogs/crossreference-anchor.js'); |
|
61 CKEDITOR.dialog.add(linkDialogCmdName, this.path + 'dialogs/crossreference-link.js'); |
|
62 |
|
63 editor.addCommand(anchorDialogCmdName, new CKEDITOR.dialogCommand(anchorDialogCmdName, { |
|
64 allowedContent: anchorAllowedContent, |
|
65 requiredContent: anchorRequiredContent |
|
66 })); |
|
67 editor.addCommand(linkDialogCmdName, new CKEDITOR.dialogCommand(linkDialogCmdName, { |
|
68 allowedContent: linkAllowedContent, |
|
69 requiredContent: linkRequiredContent |
|
70 })); |
|
71 |
|
72 // commands |
|
73 |
|
74 editor.addCommand(updateCmdName, { |
|
75 async: true, |
|
76 contextSensitive: false, |
|
77 editorFocus: false, |
|
78 modes: { |
|
79 wysiwyg: 1, |
|
80 source: 1 |
|
81 }, |
|
82 readOnly: true, |
|
83 exec: function(editor) { |
|
84 editor.setReadOnly(true); |
|
85 var notification = editor.showNotification(editor.lang.crossreference.updatingCrossReferences, 'progress', 0); |
|
86 |
|
87 var cmd = this; |
|
88 |
|
89 var typesCount = 0; |
|
90 var processedTypesCount = 0; |
|
91 for (var typeName in config.types) { |
|
92 typesCount++; |
|
93 } |
|
94 var linksCount = 0; |
|
95 |
|
96 var html = null; |
|
97 if (editor.mode == 'source') |
|
98 html = $('<div>' + editor.getData() + '</div>'); |
|
99 else |
|
100 html = $(editor.editable().$); |
|
101 |
|
102 function finishCommand() { |
|
103 editor.setReadOnly(false); |
|
104 editor.fire('afterCommandExec', { |
|
105 name: updateCmdName, |
|
106 command: cmd |
|
107 }); |
|
108 notification.update({ |
|
109 type: 'success', |
|
110 message: editor.lang.crossreference.updatedCrossReferences + linksCount, |
|
111 important: true |
|
112 }); |
|
113 } |
|
114 |
|
115 if (typesCount == 0) { |
|
116 finishCommand(); |
|
117 return; |
|
118 } |
|
119 |
|
120 for (var typeName in config.types) { |
|
121 config.findAnchors(config, editor, config.types[typeName], function(anchors) { |
|
122 notification.update({ |
|
123 progress: (1 / typesCount) * processedTypesCount |
|
124 }); |
|
125 for (var i = 0; i < anchors.length; i++) { |
|
126 var anchor = anchors[i]; |
|
127 var type = config.types[anchor.type]; |
|
128 |
|
129 notification.update({ |
|
130 progress: (1 / typesCount) * processedTypesCount + (1 / typesCount / anchors.length) * i |
|
131 }); |
|
132 |
|
133 var aName = type.type + '-' + anchor.guid; |
|
134 |
|
135 var anchorElement = $('a[cross-reference="' + type.type + '"][cross-anchor][cross-guid="' + anchor.guid + '"]', html); |
|
136 if (anchorElement.length > 0) { |
|
137 anchorElement.attr('cross-reference', type.type); |
|
138 anchorElement.attr('cross-anchor', ''); |
|
139 anchorElement.attr('cross-guid', anchor.guid); |
|
140 anchorElement.attr('cross-name', anchor.name); |
|
141 anchorElement.attr('cross-number', anchor.number); |
|
142 anchorElement.attr('name', aName); |
|
143 if (!anchorElement.hasClass('cross-reference')) |
|
144 anchorElement.addClass('cross-reference'); |
|
145 if (!anchorElement.hasClass('cross-anchor')) |
|
146 anchorElement.addClass('cross-anchor'); |
|
147 |
|
148 anchorElement.removeAttr('cross-link'); |
|
149 anchorElement.removeClass('cross-link'); |
|
150 |
|
151 anchorElement.text(anchor.text); |
|
152 } |
|
153 |
|
154 $('a[cross-reference="' + type.type + '"][cross-link][cross-guid="' + anchor.guid + '"]', html).each(function() { |
|
155 var linkElement = $(this); |
|
156 |
|
157 linkElement.attr('cross-reference', type.type); |
|
158 linkElement.attr('cross-link', ''); |
|
159 linkElement.attr('cross-guid', anchor.guid); |
|
160 linkElement.attr('cross-name', anchor.name); |
|
161 linkElement.attr('cross-number', anchor.number); |
|
162 linkElement.attr('href', '#' + aName); |
|
163 |
|
164 if (!linkElement.hasClass('cross-reference')) |
|
165 linkElement.addClass('cross-reference'); |
|
166 if (!linkElement.hasClass('cross-link')) |
|
167 linkElement.addClass('cross-link'); |
|
168 |
|
169 linkElement.removeAttr('cross-anchor'); |
|
170 linkElement.removeClass('cross-anchor'); |
|
171 |
|
172 var linkText = anchor.text; |
|
173 if (type.linkTextTemplate) |
|
174 linkText = config.formatText(type.linkTextTemplate, anchor); |
|
175 linkElement.text(linkText); |
|
176 linkElement.attr('title', anchor.text.replace(/ /g, ' ').trim()); |
|
177 |
|
178 linksCount++; |
|
179 }); |
|
180 } |
|
181 processedTypesCount++; |
|
182 if (processedTypesCount >= typesCount) { |
|
183 // done |
|
184 if (editor.mode == 'source') |
|
185 editor.setData(html.html()); |
|
186 finishCommand(); |
|
187 } |
|
188 }); |
|
189 } |
|
190 } |
|
191 }); |
|
192 editor.on('doubleclick', function(evt) { |
|
193 if (evt.data.element && !evt.data.element.isReadOnly() && evt.data.element.getName() === 'a' |
|
194 && evt.data.element.hasAttribute('cross-reference')) { |
|
195 editor.getSelection().selectElement(evt.data.element); |
|
196 if (evt.data.element.hasAttribute('cross-anchor')) { |
|
197 evt.data.dialog = anchorDialogCmdName; |
|
198 } else if (evt.data.element.hasAttribute('cross-link')) { |
|
199 evt.data.dialog = linkDialogCmdName; |
|
200 } |
|
201 } |
|
202 }); |
|
203 |
|
204 // menu |
|
205 |
|
206 var updateMenuItemName = 'updateCrossReferences'; |
|
207 var setAnchorMenuItemName = 'setCrossReferenceAnchor'; |
|
208 var setLinkMenuItemName = 'setCrossReferenceLink'; |
|
209 |
|
210 var getMenuState = function(element, alwaysAllowEditItems) { |
|
211 var items = {}; |
|
212 items[updateMenuItemName] = CKEDITOR.TRISTATE_OFF; |
|
213 if (alwaysAllowEditItems == true) { |
|
214 items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_OFF; |
|
215 items[setLinkMenuItemName] = CKEDITOR.TRISTATE_OFF; |
|
216 } |
|
217 if (element && element.getName() === 'a' && element.hasAttribute('cross-reference')) { |
|
218 items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_OFF; |
|
219 items[setLinkMenuItemName] = CKEDITOR.TRISTATE_OFF; |
|
220 if (element.hasAttribute('cross-anchor')) { |
|
221 items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_ON; |
|
222 items[setLinkMenuItemName] = CKEDITOR.TRISTATE_DISABLED; |
|
223 } |
|
224 if (element.hasAttribute('cross-link')) { |
|
225 items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_DISABLED; |
|
226 items[setLinkMenuItemName] = CKEDITOR.TRISTATE_ON; |
|
227 } |
|
228 } |
|
229 return items; |
|
230 } |
|
231 if (editor.addMenuItem) { |
|
232 editor.addMenuGroup('crossreferenceGroup'); |
|
233 editor.addMenuItem(updateMenuItemName, { |
|
234 label : editor.lang.crossreference.updateCrossReferences, |
|
235 command : updateCmdName, |
|
236 icon: this.path + 'icons/update.png', |
|
237 group : 'crossreferenceGroup' |
|
238 }); |
|
239 editor.addMenuItem(setAnchorMenuItemName, { |
|
240 label : editor.lang.crossreference.setCrossReferenceAnchor, |
|
241 command : anchorDialogCmdName, |
|
242 icon: this.path + 'icons/anchor.png', |
|
243 group : 'crossreferenceGroup' |
|
244 }); |
|
245 editor.addMenuItem(setLinkMenuItemName, { |
|
246 label : editor.lang.crossreference.setCrossReferenceLink, |
|
247 command : linkDialogCmdName, |
|
248 icon: this.path + 'icons/link.png', |
|
249 group : 'crossreferenceGroup' |
|
250 }); |
|
251 } |
|
252 if (editor.contextMenu) { |
|
253 editor.contextMenu.addListener(function(element, selection) { |
|
254 if (element.getName() === 'a' && element.hasAttribute('cross-reference')) { |
|
255 selection.selectElement(element); |
|
256 } |
|
257 var state = getMenuState(element, false); |
|
258 return state; |
|
259 }); |
|
260 } |
|
261 |
|
262 function getConfig() { |
|
263 var defaultConfig = { |
|
264 activeTypes: ['chapter', 'image', 'table', 'reference'], |
|
265 overrideTypes: false, |
|
266 types: {} |
|
267 }; |
|
268 defaultConfig.types.chapter = { |
|
269 name: editor.lang.crossreference.chapter, |
|
270 anchorTextTemplate: '${number}. ${name}.', |
|
271 linkTextTemplate: '${number}', |
|
272 numeration: { |
|
273 enabled: true, |
|
274 firstNumber: '1', |
|
275 increase: function(number) { |
|
276 var n = parseInt(number); |
|
277 return ++n; |
|
278 } |
|
279 }, |
|
280 anchorsProvider: 'default', |
|
281 allowCreateAnchors: true, |
|
282 groupAnchors: false |
|
283 }; |
|
284 defaultConfig.types.image = { |
|
285 name: editor.lang.crossreference.figure, |
|
286 anchorTextTemplate: editor.lang.crossreference.fig + ' ${number}. ${name}.', |
|
287 linkTextTemplate: '${number}', |
|
288 numeration: { |
|
289 enabled: true, |
|
290 firstNumber: '1', |
|
291 increase: function(number) { |
|
292 var n = parseInt(number); |
|
293 return ++n; |
|
294 } |
|
295 }, |
|
296 anchorsProvider: 'default', |
|
297 allowCreateAnchors: true, |
|
298 groupAnchors: false |
|
299 }; |
|
300 defaultConfig.types.table = { |
|
301 name: editor.lang.crossreference.table, |
|
302 anchorTextTemplate: editor.lang.crossreference.table + ' ${number}. ${name}.', |
|
303 linkTextTemplate: '${number}', |
|
304 numeration: { |
|
305 enabled: true, |
|
306 firstNumber: '1', |
|
307 increase: function(number) { |
|
308 var n = parseInt(number); |
|
309 return ++n; |
|
310 } |
|
311 }, |
|
312 anchorsProvider: 'default', |
|
313 allowCreateAnchors: true, |
|
314 groupAnchors: false |
|
315 }; |
|
316 defaultConfig.types.reference = { |
|
317 name: editor.lang.crossreference.reference, |
|
318 anchorTextTemplate: '[${number}] ${name}.', |
|
319 linkTextTemplate: '[${number}]', |
|
320 numeration: { |
|
321 enabled: true, |
|
322 firstNumber: '1', |
|
323 increase: function(number) { |
|
324 var n = parseInt(number); |
|
325 return ++n; |
|
326 } |
|
327 }, |
|
328 anchorsProvider: 'default', |
|
329 allowCreateAnchors: true, |
|
330 groupAnchors: false |
|
331 }; |
|
332 |
|
333 var config = CKEDITOR.tools.clone(defaultConfig); |
|
334 if (editor.config.crossreference) { |
|
335 config = CKEDITOR.tools.extend(config, editor.config.crossreference, true); |
|
336 if (!config.overrideTypes) { |
|
337 for (var typeName in defaultConfig.types) { |
|
338 var type = defaultConfig.types[typeName]; |
|
339 if (!(typeName in config.types)) |
|
340 config.types[typeName] = type; |
|
341 } |
|
342 } |
|
343 } |
|
344 for (var typeName in config.types) { |
|
345 var type = config.types[typeName]; |
|
346 type.type = typeName; |
|
347 } |
|
348 for (var typeName in config.types) { |
|
349 if ($.inArray(typeName, config.activeTypes) == -1) { |
|
350 delete config.types[typeName]; |
|
351 } |
|
352 } |
|
353 |
|
354 // shared methods |
|
355 |
|
356 config.findAnchors = function(config, editor, type, callback) { |
|
357 var anchors = []; |
|
358 |
|
359 if (type == null) { |
|
360 callback(anchors); |
|
361 return; |
|
362 } |
|
363 |
|
364 var number = null; |
|
365 if (type.numeration && type.numeration.enabled) |
|
366 number = type.numeration.firstNumber + ''; |
|
367 |
|
368 var html = null; |
|
369 if (editor.mode == 'source') |
|
370 html = $('<div>' + editor.getData() + '</div>'); |
|
371 else |
|
372 html = $(editor.editable().$); |
|
373 |
|
374 $('a[cross-reference="' + type.type + '"][cross-anchor]', html).each(function() { |
|
375 var element = $(this); |
|
376 var anchor = { |
|
377 type: element.attr('cross-reference'), |
|
378 guid: element.attr('cross-guid'), |
|
379 name: element.attr('cross-name'), |
|
380 number: number, |
|
381 text: element.text() |
|
382 } |
|
383 anchors.push(anchor); |
|
384 if (type.numeration && type.numeration.enabled) |
|
385 number = type.numeration.increase(number); |
|
386 }); |
|
387 |
|
388 function postProcessAnchors(anchors) { |
|
389 for(var i = 0; i < anchors.length; i++) { |
|
390 var anchor = anchors[i]; |
|
391 |
|
392 if (anchor.type != type.type) |
|
393 throw 'Incompatible type: ' + type.type; |
|
394 |
|
395 var text = anchor.name; |
|
396 if (type.anchorTextTemplate) { |
|
397 text = config.formatText(type.anchorTextTemplate, anchor); |
|
398 } |
|
399 anchor.text = text; |
|
400 } |
|
401 callback(anchors); |
|
402 } |
|
403 |
|
404 if (type.anchorsProvider && type.anchorsProvider !== 'default') { |
|
405 type.anchorsProvider(postProcessAnchors, anchors, type, editor); |
|
406 } else { |
|
407 postProcessAnchors(anchors); |
|
408 } |
|
409 }; |
|
410 |
|
411 config.formatText = function(template, anchor) { |
|
412 var text = template; |
|
413 |
|
414 for (var propName in anchor) { |
|
415 var propValue = anchor[propName]; |
|
416 var regexp = new RegExp('\\$\\{' + propName + '\\}', 'g'); |
|
417 if (propValue) |
|
418 text = text.replace(regexp, propValue); |
|
419 else |
|
420 text = text.replace(regexp, ''); |
|
421 } |
|
422 |
|
423 if (anchor.level != null) { |
|
424 var shift = ''; |
|
425 for (var i = 0; i < anchor.level; i++) |
|
426 shift += ' '; |
|
427 |
|
428 text = text.replace(/\$\{levelShift\}/g, shift); |
|
429 } |
|
430 |
|
431 text = text.trim(); |
|
432 |
|
433 return text; |
|
434 } |
|
435 |
|
436 return config; |
|
437 } |
|
438 } |
|
439 }); |