ckeditor/plugins/crossreference/plugin.js
author indvd00m (gotoindvdum[at]gmail[dot]com)
Thu, 15 Dec 2016 18:10:20 +0300
changeset 0 44d330dccc59
child 2 467e24fbc60e
permissions -rw-r--r--
Init sample
     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(/&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 		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 += '&nbsp;&nbsp;';
   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 });