ckeditor/plugins/crossreference/plugin.js
author indvdum (gotoindvdum[at]gmail[dot]com)
Wed, 21 Dec 2016 17:20:19 +0300
changeset 5 c925ae656709
parent 4 40e26009689c
permissions -rw-r--r--
Update crossreference plugin
     1 CKEDITOR.plugins.add('crossreference', {
     2 	lang : [ 'en', 'ru' ],
     3 	requires : 'dialog,notification',
     4 	icons : 'crossreference,crossreference-anchor,crossreference-link,crossreference-update',
     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(/&nbsp;/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 		
   193 		// keystrokes
   194 		
   195 		editor.setKeystroke(CKEDITOR.CTRL + CKEDITOR.SHIFT + 65, anchorDialogCmdName);
   196 		editor.setKeystroke(CKEDITOR.CTRL + CKEDITOR.SHIFT + 76, linkDialogCmdName);
   197 		editor.setKeystroke(CKEDITOR.CTRL + CKEDITOR.ALT + 85, updateCmdName);
   198 		
   199 		// double click
   200 		
   201 		editor.on('doubleclick', function(evt) {
   202 			if (evt.data.element && !evt.data.element.isReadOnly() && evt.data.element.getName() === 'a' 
   203 					&& evt.data.element.hasAttribute('cross-reference')) {
   204 				editor.getSelection().selectElement(evt.data.element);
   205 				if (evt.data.element.hasAttribute('cross-anchor')) {
   206 					evt.data.dialog = anchorDialogCmdName;
   207 				} else if (evt.data.element.hasAttribute('cross-link')) {
   208 					evt.data.dialog = linkDialogCmdName;
   209 				}
   210 			}
   211 		});
   212 		
   213 		// menu
   214 		
   215 		var updateMenuItemName = 'updateCrossReferences';
   216 		var setAnchorMenuItemName = 'setCrossReferenceAnchor';
   217 		var setLinkMenuItemName = 'setCrossReferenceLink';
   218 		
   219 		var getMenuState = function(element, alwaysAllowEditItems) {
   220 			var items = {};
   221 			items[updateMenuItemName] = CKEDITOR.TRISTATE_OFF;
   222 			if (alwaysAllowEditItems == true) {
   223 				items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_OFF;
   224 				items[setLinkMenuItemName] = CKEDITOR.TRISTATE_OFF;
   225 			}
   226 			if (element && element.getName() === 'a' && element.hasAttribute('cross-reference')) {
   227 				items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_OFF;
   228 				items[setLinkMenuItemName] = CKEDITOR.TRISTATE_OFF;
   229 				if (element.hasAttribute('cross-anchor')) {
   230 					items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_ON;
   231 					items[setLinkMenuItemName] = CKEDITOR.TRISTATE_DISABLED;
   232 				}
   233 				if (element.hasAttribute('cross-link')) {
   234 					items[setAnchorMenuItemName] = CKEDITOR.TRISTATE_DISABLED;
   235 					items[setLinkMenuItemName] = CKEDITOR.TRISTATE_ON;
   236 				}
   237 			}
   238 			return items;
   239 		}
   240 		if (editor.addMenuItem) {
   241 			editor.addMenuGroup('crossreferenceGroup');
   242 			editor.addMenuItem(updateMenuItemName, {
   243 				label : editor.lang.crossreference.updateCrossReferences,
   244 				command : updateCmdName,
   245 				icon: 'crossreference-update',
   246 				group : 'crossreferenceGroup'
   247 			});
   248 			editor.addMenuItem(setAnchorMenuItemName, {
   249 				label : editor.lang.crossreference.setCrossReferenceAnchor,
   250 				command : anchorDialogCmdName,
   251 				icon: 'crossreference-anchor',
   252 				group : 'crossreferenceGroup'
   253 			});
   254 			editor.addMenuItem(setLinkMenuItemName, {
   255 				label : editor.lang.crossreference.setCrossReferenceLink,
   256 				command : linkDialogCmdName,
   257 				icon: 'crossreference-link',
   258 				group : 'crossreferenceGroup'
   259 			});
   260 		}
   261 		if (editor.contextMenu) {
   262 			editor.contextMenu.addListener(function(element, selection) {
   263 				if (element.getName() === 'a' && element.hasAttribute('cross-reference')) {
   264 					selection.selectElement(element);
   265 				}
   266 				var state = getMenuState(element, false);
   267 				return state;
   268 			});
   269 		}
   270 		
   271 		function getConfig() {
   272 			var defaultConfig = {
   273 				activeTypes: ['chapter', 'image', 'table', 'reference'],
   274 				overrideTypes: false,
   275 				types: {}
   276 			};
   277 			defaultConfig.types.chapter = {
   278 				name: editor.lang.crossreference.chapter,
   279 				anchorTextTemplate: '${number}. ${name}.',
   280 				linkTextTemplate: '${number}',
   281 				numeration: {
   282 					enabled: true,
   283 					firstNumber: '1',
   284 					increase: function(number) {
   285 						var n = parseInt(number);
   286 						return ++n;
   287 					}
   288 				},
   289 				anchorsProvider: 'default',
   290 				allowCreateAnchors: true,
   291 				groupAnchors: false
   292 			};
   293 			defaultConfig.types.image = {
   294 				name: editor.lang.crossreference.figure,
   295 				anchorTextTemplate: editor.lang.crossreference.fig + ' ${number}. ${name}.',
   296 				linkTextTemplate: '${number}',
   297 				numeration: {
   298 					enabled: true,
   299 					firstNumber: '1',
   300 					increase: function(number) {
   301 						var n = parseInt(number);
   302 						return ++n;
   303 					}
   304 				},
   305 				anchorsProvider: 'default',
   306 				allowCreateAnchors: true,
   307 				groupAnchors: false
   308 			};
   309 			defaultConfig.types.table = {
   310 				name: editor.lang.crossreference.table,
   311 				anchorTextTemplate: editor.lang.crossreference.table + ' ${number}. ${name}.',
   312 				linkTextTemplate: '${number}',
   313 				numeration: {
   314 					enabled: true,
   315 					firstNumber: '1',
   316 					increase: function(number) {
   317 						var n = parseInt(number);
   318 						return ++n;
   319 					}
   320 				},
   321 				anchorsProvider: 'default',
   322 				allowCreateAnchors: true,
   323 				groupAnchors: false
   324 			};
   325 			defaultConfig.types.reference = {
   326 				name: editor.lang.crossreference.reference,
   327 				anchorTextTemplate: '[${number}] ${name}.',
   328 				linkTextTemplate: '[${number}]',
   329 				numeration: {
   330 					enabled: true,
   331 					firstNumber: '1',
   332 					increase: function(number) {
   333 						var n = parseInt(number);
   334 						return ++n;
   335 					}
   336 				},
   337 				anchorsProvider: 'default',
   338 				allowCreateAnchors: true,
   339 				groupAnchors: false
   340 			};
   341 			
   342 			var config = CKEDITOR.tools.clone(defaultConfig);
   343 			if (editor.config.crossreference) {
   344 				config = CKEDITOR.tools.extend(config, editor.config.crossreference, true);
   345 				if (!config.overrideTypes) {
   346 					for (var typeName in defaultConfig.types) {
   347 						var type = defaultConfig.types[typeName];
   348 						if (!(typeName in config.types))
   349 							config.types[typeName] = type;
   350 					}
   351 				}
   352 			}
   353 			for (var typeName in config.types) {
   354 				var type = config.types[typeName];
   355 				type.type = typeName;
   356 			}
   357 			for (var typeName in config.types) {
   358 				if ($.inArray(typeName, config.activeTypes) == -1) {
   359 					delete config.types[typeName];
   360 				}
   361 			}
   362 			
   363 			// shared methods
   364 			
   365 			config.findAnchors = function(config, editor, type, callback) {
   366 				var anchors = [];
   367 				
   368 				if (type == null) {
   369 					callback(anchors);
   370 					return;
   371 				}
   372 				
   373 				var number = null;
   374 				if (type.numeration && type.numeration.enabled)
   375 					number = type.numeration.firstNumber + '';
   376 				
   377 				var html = null;
   378 				if (editor.mode == 'source')
   379 					html = $('<div>' + editor.getData() + '</div>');
   380 				else
   381 					html = $(editor.editable().$);
   382 				
   383 				$('a[cross-reference="' + type.type + '"][cross-anchor]', html).each(function() {
   384 					var element = $(this);
   385 					var anchor = {
   386 						type: element.attr('cross-reference'),
   387 						guid: element.attr('cross-guid'),
   388 						name: element.attr('cross-name'),
   389 						number: number,
   390 						text: element.text()
   391 					}
   392 					anchors.push(anchor);
   393 					if (type.numeration && type.numeration.enabled)
   394 						number = type.numeration.increase(number);
   395 				});
   396 				
   397 				function postProcessAnchors(anchors) {
   398 					for(var i = 0; i < anchors.length; i++) {
   399 						var anchor = anchors[i];
   400 						
   401 						if (anchor.type != type.type)
   402 							throw 'Incompatible type: ' + type.type;
   403 						
   404 						var text = anchor.name;
   405 						if (type.anchorTextTemplate) {
   406 							text = config.formatText(type.anchorTextTemplate, anchor);
   407 						}
   408 						anchor.text = text;
   409 					}
   410 					callback(anchors);
   411 				}
   412 				
   413 				if (type.anchorsProvider && type.anchorsProvider !== 'default') {
   414 					type.anchorsProvider(postProcessAnchors, anchors, type, editor);
   415 				} else {
   416 					postProcessAnchors(anchors);
   417 				}
   418 			};
   419 			
   420 			config.formatText = function(template, anchor) {
   421 				var text = template;
   422 				
   423 				for (var propName in anchor) {
   424 					var propValue = anchor[propName];
   425 					var regexp = new RegExp('\\$\\{' + propName + '\\}', 'g');
   426 					if (propValue)
   427 						text = text.replace(regexp, propValue);
   428 					else
   429 						text = text.replace(regexp, '');
   430 				}
   431 				
   432 				if (anchor.level != null) {
   433 					var shift = '';
   434 					for (var i = 0; i < anchor.level; i++)
   435 						shift += '&nbsp;&nbsp;';
   436 					
   437 					text = text.replace(/\$\{levelShift\}/g, shift);
   438 				}
   439 				
   440 				text = text.replace(/\s+/g, ' ');
   441 				text = text.trim();
   442 				
   443 				return text;
   444 			}
   445 			
   446 			return config;
   447 		}
   448 	}
   449 });