@oat-sa/tao-core-ui 1.58.1 → 1.58.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actionbar.js +386 -395
- package/dist/adder.js +21 -19
- package/dist/animable/absorbable/absorbable.js +204 -213
- package/dist/animable/absorbable/css/absorb.css +1 -0
- package/dist/animable/absorbable/css/absorb.css.map +1 -1
- package/dist/animable/pulsable/pulsable.js +168 -177
- package/dist/autocomplete/css/autocomplete.css +1 -0
- package/dist/autocomplete/css/autocomplete.css.map +1 -1
- package/dist/autocomplete.js +68 -66
- package/dist/badge/badge.js +188 -197
- package/dist/badge/css/badge.css +1 -0
- package/dist/badge/css/badge.css.map +1 -1
- package/dist/breadcrumbs.js +275 -284
- package/dist/btngrouper.js +5 -5
- package/dist/bulkActionPopup.js +490 -495
- package/dist/button.js +283 -291
- package/dist/cascadingComboBox.js +249 -258
- package/dist/ckeditor/ckConfigurator.js +26 -19
- package/dist/ckeditor/dtdHandler.js +11 -9
- package/dist/class/selector.js +441 -450
- package/dist/component/resizable.js +1 -1
- package/dist/component/windowed.js +285 -294
- package/dist/component.js +419 -428
- package/dist/contextualPopup.js +417 -426
- package/dist/dashboard.js +300 -309
- package/dist/datalist.js +753 -762
- package/dist/datatable/filterStrategy/multiple.js +1 -1
- package/dist/datatable/filterStrategy/single.js +1 -1
- package/dist/datatable.js +1527 -1550
- package/dist/dateRange/dateRange.js +393 -402
- package/dist/datetime/picker.js +665 -672
- package/dist/deleter.js +368 -377
- package/dist/destination/selector.js +286 -295
- package/dist/dialog/alert.js +3 -3
- package/dist/dialog/confirm.js +1 -1
- package/dist/dialog/confirmDelete.js +216 -225
- package/dist/dialog.js +650 -654
- package/dist/disabler.js +8 -8
- package/dist/documentViewer/providers/pdfViewer/fallback/viewer.js +166 -175
- package/dist/documentViewer/providers/pdfViewer/pdfjs/findBar.js +518 -527
- package/dist/documentViewer/providers/pdfViewer/pdfjs/pageView.js +380 -389
- package/dist/documentViewer/providers/pdfViewer/pdfjs/searchEngine.js +539 -548
- package/dist/documentViewer/providers/pdfViewer/pdfjs/viewer.js +369 -378
- package/dist/documentViewer/providers/pdfViewer.js +184 -193
- package/dist/documentViewer.js +292 -301
- package/dist/dropdown.js +383 -392
- package/dist/durationer.js +5 -5
- package/dist/dynamicComponent.js +597 -598
- package/dist/feedback.js +356 -362
- package/dist/figure/FigureStateActive.js +117 -108
- package/dist/filesender.js +2 -2
- package/dist/filter.js +230 -239
- package/dist/form/dropdownForm.js +355 -357
- package/dist/form/form.js +919 -690
- package/dist/form/simpleForm.js +1 -1
- package/dist/form/validator/renderer.js +233 -235
- package/dist/form/validator/validator.js +257 -189
- package/dist/form/widget/definitions.js +1 -1
- package/dist/form/widget/providers/checkBox.js +254 -259
- package/dist/form/widget/providers/comboBox.js +187 -192
- package/dist/form/widget/providers/default.js +8 -9
- package/dist/form/widget/providers/hidden.js +170 -179
- package/dist/form/widget/providers/hiddenBox.js +262 -267
- package/dist/form/widget/providers/radioBox.js +216 -225
- package/dist/form/widget/providers/textArea.js +187 -196
- package/dist/form/widget/providers/textBox.js +2 -3
- package/dist/form/widget/widget.js +473 -475
- package/dist/formValidator/formValidator.js +1 -1
- package/dist/formValidator/highlighters/message.js +1 -1
- package/dist/generis/form/form.js +314 -323
- package/dist/generis/validator/validator.js +209 -218
- package/dist/generis/widget/checkBox/checkBox.js +218 -227
- package/dist/generis/widget/comboBox/comboBox.js +179 -188
- package/dist/generis/widget/hiddenBox/hiddenBox.js +220 -229
- package/dist/generis/widget/textBox/textBox.js +169 -178
- package/dist/generis/widget/widget.js +246 -255
- package/dist/groupedComboBox.js +222 -231
- package/dist/groupvalidator.js +2 -2
- package/dist/highlighter.js +967 -958
- package/dist/image/ImgStateActive/helper.js +7 -5
- package/dist/image/ImgStateActive/initHelper.js +49 -43
- package/dist/image/ImgStateActive/initMediaEditor.js +24 -20
- package/dist/image/ImgStateActive/mediaSizer.js +14 -12
- package/dist/image/ImgStateActive.js +72 -70
- package/dist/incrementer.js +6 -6
- package/dist/inplacer.js +6 -6
- package/dist/itemButtonList/css/item-button-list.css +1 -0
- package/dist/itemButtonList/css/item-button-list.css.map +1 -1
- package/dist/itemButtonList.js +439 -435
- package/dist/keyNavigation/navigableDomElement.js +51 -38
- package/dist/keyNavigation/navigator.js +85 -70
- package/dist/listbox.js +460 -469
- package/dist/liststyler.js +8 -8
- package/dist/loadingButton/loadingButton.js +209 -218
- package/dist/lock.js +476 -485
- package/dist/login/login.js +475 -484
- package/dist/maths/calculator/basicCalculator.js +235 -244
- package/dist/maths/calculator/calculatorComponent.js +3 -3
- package/dist/maths/calculator/core/board.js +772 -781
- package/dist/maths/calculator/core/expression.js +476 -485
- package/dist/maths/calculator/core/labels.js +228 -237
- package/dist/maths/calculator/core/tokenizer.js +1 -1
- package/dist/maths/calculator/core/tokens.js +163 -170
- package/dist/maths/calculator/plugins/keyboard/templateKeyboard/templateKeyboard.js +244 -253
- package/dist/maths/calculator/plugins/screen/simpleScreen/simpleScreen.js +279 -288
- package/dist/maths/calculator/scientificCalculator.js +327 -336
- package/dist/mediaEditor/mediaEditorComponent.js +238 -245
- package/dist/mediaEditor/plugins/mediaAlignment/helper.js +7 -7
- package/dist/mediaEditor/plugins/mediaAlignment/mediaAlignmentComponent.js +229 -235
- package/dist/mediaEditor/plugins/mediaDimension/mediaDimensionComponent.js +580 -589
- package/dist/mediaplayer/players/html5.js +666 -675
- package/dist/mediaplayer/players/youtube.js +419 -424
- package/dist/mediaplayer/support.js +11 -10
- package/dist/mediaplayer/utils/reminder.js +14 -13
- package/dist/mediaplayer/utils/timeObserver.js +10 -11
- package/dist/mediaplayer/youtubeManager.js +164 -145
- package/dist/mediaplayer.js +1565 -1520
- package/dist/mediasizer.js +669 -678
- package/dist/modal.js +10 -17
- package/dist/pageSizeSelector.js +219 -228
- package/dist/pagination/providers/pages.js +280 -289
- package/dist/pagination/providers/simple.js +192 -201
- package/dist/previewer.js +30 -30
- package/dist/progressbar.js +4 -4
- package/dist/report.js +347 -356
- package/dist/resource/filters.js +271 -280
- package/dist/resource/list.js +1264 -1273
- package/dist/resource/selector.js +865 -874
- package/dist/resource/tree.js +1483 -1492
- package/dist/resourcemgr/fileBrowser.js +564 -569
- package/dist/resourcemgr/filePreview.js +16 -16
- package/dist/resourcemgr/fileSelector.js +515 -524
- package/dist/resourcemgr/util/updatePermissions.js +2 -2
- package/dist/resourcemgr.js +306 -315
- package/dist/searchModal/advancedSearch.js +796 -767
- package/dist/searchModal.js +114 -91
- package/dist/switch/switch.js +298 -307
- package/dist/tabs.js +598 -575
- package/dist/taskQueue/status.js +312 -321
- package/dist/taskQueue/table.js +375 -384
- package/dist/taskQueue/taskQueueModel.js +488 -472
- package/dist/taskQueueButton/taskable.js +264 -273
- package/dist/taskQueueButton/treeButton.js +189 -198
- package/dist/themeLoader.js +24 -23
- package/dist/themes.js +1 -1
- package/dist/toggler.js +3 -3
- package/dist/tooltip.js +295 -304
- package/dist/transformer.js +2 -2
- package/dist/tristateCheckboxGroup.js +311 -320
- package/dist/uploader.js +687 -696
- package/dist/validator/Report.js +1 -1
- package/dist/validator/Validator.js +3 -3
- package/dist/validator/validators.js +9 -9
- package/dist/validator.js +240 -230
- package/dist/waitForMedia.js +1 -1
- package/package.json +3 -3
- package/src/animable/absorbable/css/absorb.css +1 -0
- package/src/animable/absorbable/css/absorb.css.map +1 -1
- package/src/autocomplete/css/autocomplete.css +1 -0
- package/src/autocomplete/css/autocomplete.css.map +1 -1
- package/src/badge/css/badge.css +1 -0
- package/src/badge/css/badge.css.map +1 -1
- package/src/ckeditor/ckConfigurator.js +4 -0
- package/src/itemButtonList/css/item-button-list.css +1 -0
- package/src/itemButtonList/css/item-button-list.css.map +1 -1
- package/src/.DS_Store +0 -0
- package/src/css/basic.css +0 -7826
- package/src/css/basic.css.map +0 -1
- package/src/css/ckeditor/skins/tao/css/dialog.css +0 -950
- package/src/css/ckeditor/skins/tao/css/dialog.css.map +0 -1
- package/src/css/ckeditor/skins/tao/css/editor.css +0 -1850
- package/src/css/ckeditor/skins/tao/css/editor.css.map +0 -1
- package/src/scss/.DS_Store +0 -0
- package/src/scss/basic.scss +0 -16
- package/src/scss/ckeditor/skins/tao/scss/dialog.scss +0 -763
- package/src/scss/ckeditor/skins/tao/scss/editor.scss +0 -111
- package/src/scss/ckeditor/skins/tao/scss/inc/_ck-icons.scss +0 -59
- package/src/scss/ckeditor/skins/tao/scss/inc/_colorpanel.scss +0 -118
- package/src/scss/ckeditor/skins/tao/scss/inc/_elementspath.scss +0 -69
- package/src/scss/ckeditor/skins/tao/scss/inc/_mainui.scss +0 -194
- package/src/scss/ckeditor/skins/tao/scss/inc/_menu.scss +0 -181
- package/src/scss/ckeditor/skins/tao/scss/inc/_panel.scss +0 -200
- package/src/scss/ckeditor/skins/tao/scss/inc/_presets.scss +0 -32
- package/src/scss/ckeditor/skins/tao/scss/inc/_reset.scss +0 -101
- package/src/scss/ckeditor/skins/tao/scss/inc/_richcombo.scss +0 -213
- package/src/scss/ckeditor/skins/tao/scss/inc/_tao.scss +0 -59
- package/src/scss/ckeditor/skins/tao/scss/inc/_toolbar.scss +0 -301
- package/src/scss/font/source-sans-pro/source-sans-pro-italic.eot +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-italic.eot.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-italic.woff +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-italic.woff.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-regular.eot +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-regular.eot.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-regular.woff +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-regular.woff.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold-italic.eot +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold-italic.eot.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold-italic.woff +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold-italic.woff.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold.eot +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold.eot.b64 +0 -1
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold.woff +0 -0
- package/src/scss/font/source-sans-pro/source-sans-pro-semibold.woff.b64 +0 -1
- package/src/scss/font/tao/tao.eot +0 -0
- package/src/scss/font/tao/tao.svg +0 -235
- package/src/scss/font/tao/tao.ttf +0 -0
- package/src/scss/font/tao/tao.woff +0 -0
- package/src/scss/inc/_base.scss +0 -496
- package/src/scss/inc/_bootstrap.scss +0 -6
- package/src/scss/inc/_buttons.scss +0 -114
- package/src/scss/inc/_colors.scss +0 -88
- package/src/scss/inc/_feedback.scss +0 -150
- package/src/scss/inc/_flex-grid.scss +0 -15
- package/src/scss/inc/_fonts.scss +0 -4
- package/src/scss/inc/_forms.scss +0 -827
- package/src/scss/inc/_functions.scss +0 -283
- package/src/scss/inc/_grid.scss +0 -66
- package/src/scss/inc/_jquery.nouislider.scss +0 -254
- package/src/scss/inc/_normalize.scss +0 -528
- package/src/scss/inc/_report.scss +0 -68
- package/src/scss/inc/_secondary-properties.scss +0 -89
- package/src/scss/inc/_select2.scss +0 -634
- package/src/scss/inc/_toolbars.scss +0 -155
- package/src/scss/inc/_tooltip.scss +0 -312
- package/src/scss/inc/_variables.scss +0 -21
- package/src/scss/inc/base/_highlight.scss +0 -5
- package/src/scss/inc/base/_list-style.scss +0 -59
- package/src/scss/inc/base/_svg.scss +0 -3
- package/src/scss/inc/base/_table.scss +0 -63
- package/src/scss/inc/fonts/_source-sans-pro.scss +0 -29
- package/src/scss/inc/fonts/_tao-icon-classes.scss +0 -226
- package/src/scss/inc/fonts/_tao-icon-def.scss +0 -12
- package/src/scss/inc/fonts/_tao-icon-vars.scss +0 -240
package/dist/highlighter.js
CHANGED
|
@@ -1,1199 +1,1208 @@
|
|
|
1
1
|
define(['lodash', 'jquery'], function (_, $) { 'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
|
|
4
|
+
$ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $;
|
|
5
|
+
|
|
6
|
+
function _typeof(obj) {
|
|
7
|
+
"@babel/helpers - typeof";
|
|
8
|
+
|
|
9
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
|
|
10
|
+
return typeof obj;
|
|
11
|
+
} : function (obj) {
|
|
12
|
+
return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
|
13
|
+
}, _typeof(obj);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Data attribute used to logically group the wrapping nodes into a single selection
|
|
18
|
+
* @type {string}
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
var GROUP_ATTR = 'data-hl-group';
|
|
22
|
+
/**
|
|
23
|
+
* Children of those nodes types cannot be highlighted
|
|
24
|
+
* @type {string[]}
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
var defaultBlackList = ['textarea', 'math', 'script', '.select2-container'];
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {String} options.className - name of the class that will be used by the wrappers tags to highlight text
|
|
31
|
+
* @param {String} options.containerSelector - allows to select the root Node in which highlighting is allowed
|
|
32
|
+
* @param {Array<String>} [options.containersBlackList] - additional blacklist selectors to be added to module instance's blacklist
|
|
33
|
+
* @param {Array<String>} [options.containersWhiteList] - whitelist selectors; supported only in `keepEmptyNodes` mode.
|
|
34
|
+
* Priority of blacklist or whitelist is decided by which selector is closest to the node. If no match found, node is considered whitelisted.
|
|
35
|
+
* @param {Boolean} [options.clearOnClick] - clear single highlight node on click
|
|
36
|
+
* @param {Object} [options.colors] - keys is keeping as the "c" value of storing/restore the highlighters for indexing, values are wrappers class names
|
|
37
|
+
* @param {Boolean} [options.keepEmptyNodes] - retain original dom structure as far as possible and do not remove empty nodes if they were not created by highlighter
|
|
38
|
+
* @returns {Object} - the highlighter instance
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
function highlighter (options) {
|
|
42
|
+
var className = options.className;
|
|
43
|
+
var containerSelector = options.containerSelector;
|
|
44
|
+
var keepEmptyNodes = options.keepEmptyNodes;
|
|
45
|
+
var highlightingClasses = [className]; // Multi-color mode
|
|
46
|
+
|
|
47
|
+
if (options.colors) {
|
|
48
|
+
highlightingClasses = Object.values(options.colors);
|
|
49
|
+
}
|
|
6
50
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* of the License (non-upgradable).
|
|
11
|
-
*
|
|
12
|
-
* This program is distributed in the hope that it will be useful,
|
|
13
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
-
* GNU General Public License for more details.
|
|
16
|
-
*
|
|
17
|
-
* You should have received a copy of the GNU General Public License
|
|
18
|
-
* along with this program; if not, write to the Free Software
|
|
19
|
-
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
20
|
-
*
|
|
21
|
-
* Copyright (c) 2016-2021 (original work) Open Assessment Technologies SA;
|
|
51
|
+
* list of node selectors which should NOT receive any highlighting from this instance
|
|
52
|
+
* an optional passed-in blacklist is merged with local defaults
|
|
53
|
+
* @type {Array}
|
|
22
54
|
*/
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
var containersBlackList = _.union(defaultBlackList, options.containersBlackList);
|
|
58
|
+
|
|
59
|
+
var containersBlackListSelector = containersBlackList.join(', ');
|
|
60
|
+
var containersWhiteListSelector = null;
|
|
61
|
+
var containersBlackAndWhiteListSelector = containersBlackListSelector;
|
|
62
|
+
|
|
63
|
+
if (options.keepEmptyNodes && options.containersWhiteList) {
|
|
64
|
+
containersWhiteListSelector = options.containersWhiteList.join(', ');
|
|
65
|
+
containersBlackAndWhiteListSelector = _.union(containersBlackList, options.containersWhiteList).join(', ');
|
|
66
|
+
}
|
|
23
67
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @type {
|
|
68
|
+
* used in recursive loops to decide if we should wrap or not the current node
|
|
69
|
+
* @type {boolean}
|
|
26
70
|
*/
|
|
27
71
|
|
|
28
|
-
|
|
72
|
+
|
|
73
|
+
var isWrapping = false;
|
|
29
74
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @type {
|
|
75
|
+
* performance improvement to break out of a potentially big recursive loop once the wrapping has ended
|
|
76
|
+
* @type {boolean}
|
|
32
77
|
*/
|
|
33
78
|
|
|
34
|
-
var
|
|
79
|
+
var hasWrapped = false;
|
|
35
80
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
-
* @param {String} options.containerSelector - allows to select the root Node in which highlighting is allowed
|
|
39
|
-
* @param {Array<String>} [options.containersBlackList] - additional blacklist selectors to be added to module instance's blacklist
|
|
40
|
-
* @param {Array<String>} [options.containersWhiteList] - whitelist selectors; supported only in `keepEmptyNodes` mode.
|
|
41
|
-
* Priority of blacklist or whitelist is decided by which selector is closest to the node. If no match found, node is considered whitelisted.
|
|
42
|
-
* @param {Boolean} [options.clearOnClick] - clear single highlight node on click
|
|
43
|
-
* @param {Object} [options.colors] - keys is keeping as the "c" value of storing/restore the highlighters for indexing, values are wrappers class names
|
|
44
|
-
* @param {Boolean} [options.keepEmptyNodes] - retain original dom structure as far as possible and do not remove empty nodes if they were not created by highlighter
|
|
45
|
-
* @returns {Object} - the highlighter instance
|
|
81
|
+
* used in recursive loops to assign a group Id to the current wrapped node
|
|
82
|
+
* @type {number}
|
|
46
83
|
*/
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (options.colors) {
|
|
55
|
-
highlightingClasses = Object.values(options.colors);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* list of node selectors which should NOT receive any highlighting from this instance
|
|
59
|
-
* an optional passed-in blacklist is merged with local defaults
|
|
60
|
-
* @type {Array}
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
var containersBlackList = _.union(defaultBlackList, options.containersBlackList);
|
|
65
|
-
|
|
66
|
-
var containersBlackListSelector = containersBlackList.join(', ');
|
|
67
|
-
var containersWhiteListSelector = null;
|
|
68
|
-
var containersBlackAndWhiteListSelector = containersBlackListSelector;
|
|
69
|
-
|
|
70
|
-
if (options.keepEmptyNodes && options.containersWhiteList) {
|
|
71
|
-
containersWhiteListSelector = options.containersWhiteList.join(', ');
|
|
72
|
-
containersBlackAndWhiteListSelector = _.union(containersBlackList, options.containersWhiteList).join(', ');
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* used in recursive loops to decide if we should wrap or not the current node
|
|
76
|
-
* @type {boolean}
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
var isWrapping = false;
|
|
81
|
-
/**
|
|
82
|
-
* performance improvement to break out of a potentially big recursive loop once the wrapping has ended
|
|
83
|
-
* @type {boolean}
|
|
84
|
-
*/
|
|
85
|
-
|
|
86
|
-
var hasWrapped = false;
|
|
87
|
-
/**
|
|
88
|
-
* used in recursive loops to assign a group Id to the current wrapped node
|
|
89
|
-
* @type {number}
|
|
90
|
-
*/
|
|
91
|
-
|
|
92
|
-
var currentGroupId;
|
|
93
|
-
/**
|
|
94
|
-
* used in recursive loops to build the index of text nodes
|
|
95
|
-
* @type {number}
|
|
96
|
-
*/
|
|
85
|
+
var currentGroupId;
|
|
86
|
+
/**
|
|
87
|
+
* used in recursive loops to build the index of text nodes
|
|
88
|
+
* @type {number}
|
|
89
|
+
*/
|
|
97
90
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
var textNodesIndex;
|
|
92
|
+
/**
|
|
93
|
+
* Returns the node in which highlighting is allowed
|
|
94
|
+
* @returns {Element}
|
|
95
|
+
*/
|
|
103
96
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
function getContainer() {
|
|
98
|
+
return $(containerSelector).get(0);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Returns all highlighted nodes, excluding any inside blacklisted elements
|
|
102
|
+
* @returns {JQuery<HTMLElement>}
|
|
103
|
+
*/
|
|
111
104
|
|
|
112
105
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
106
|
+
function getHighlightedNodes() {
|
|
107
|
+
return $(containerSelector).find(".".concat(highlightingClasses.join(',.'))).filter(function (i, node) {
|
|
108
|
+
return !isBlacklisted(node);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Attach data to wrapper node.
|
|
113
|
+
* Use it when deleting this highlight to know if highlight content should be merged with neighbour text nodes or not.
|
|
114
|
+
* Use it when building/restoring index to know if restored highlight content should be split off neighbour text node or not.
|
|
115
|
+
* Needed to keep markup the same as it was before highlighting.
|
|
116
|
+
* @param {HTMLElement} node
|
|
117
|
+
* @param {Boolean} beforeWasSplit
|
|
118
|
+
* @param {Boolean} afterWasSplit
|
|
119
|
+
*/
|
|
125
120
|
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
function addSplitData(node, beforeWasSplit, afterWasSplit) {
|
|
123
|
+
node.dataset.beforeWasSplit = beforeWasSplit;
|
|
124
|
+
node.dataset.afterWasSplit = afterWasSplit;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Highlight all text nodes within each given range
|
|
128
|
+
* @param {Range[]} ranges - array of ranges to highlight, may be given by the helper selector.getAllRanges()
|
|
129
|
+
*/
|
|
135
130
|
|
|
136
131
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
function highlightRanges(ranges) {
|
|
133
|
+
ranges.forEach(function (range) {
|
|
134
|
+
var rangeInfos;
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
if (isRangeValid(range)) {
|
|
137
|
+
currentGroupId = getAvailableGroupId(); // easy peasy: highlighting a plain text without any DOM nodes
|
|
138
|
+
// NOTE: The condition checks the whole node content and not a selected content in a given range, that allows to wrap whitespace
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
if (isWrappable(range.commonAncestorContainer) && !isWrappingNode(range.commonAncestorContainer.parentNode)) {
|
|
141
|
+
var wrapperNode = getWrapper(currentGroupId);
|
|
147
142
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} else {
|
|
151
|
-
addSplitData(wrapperNode, range.startOffset > 0, range.endOffset < range.commonAncestorContainer.length);
|
|
152
|
-
rangeSurroundContentsNoEmptyNodes(range, wrapperNode);
|
|
153
|
-
}
|
|
154
|
-
} else if (isWrappable(range.commonAncestorContainer) && isWrappingNode(range.commonAncestorContainer.parentNode) && range.commonAncestorContainer.parentNode !== className) {
|
|
155
|
-
highlightContainerNodes(range.commonAncestorContainer, className, range, currentGroupId); // now the fun stuff: highlighting a mix of text and DOM nodes
|
|
143
|
+
if (!keepEmptyNodes) {
|
|
144
|
+
range.surroundContents(wrapperNode);
|
|
156
145
|
} else {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
startNodeContainer: range.startContainer,
|
|
160
|
-
startOffset: range.startOffset,
|
|
161
|
-
endNode: isElement(range.endContainer) && range.endOffset > 0 ? range.endContainer.childNodes[range.endOffset - 1] : range.endContainer,
|
|
162
|
-
endNodeContainer: range.endContainer,
|
|
163
|
-
endOffset: range.endOffset,
|
|
164
|
-
commonRange: range
|
|
165
|
-
};
|
|
166
|
-
isWrapping = false;
|
|
167
|
-
hasWrapped = false;
|
|
168
|
-
wrapTextNodesInRange(range.commonAncestorContainer, rangeInfos);
|
|
146
|
+
addSplitData(wrapperNode, range.startOffset > 0, range.endOffset < range.commonAncestorContainer.length);
|
|
147
|
+
rangeSurroundContentsNoEmptyNodes(range, wrapperNode);
|
|
169
148
|
}
|
|
149
|
+
} else if (isWrappable(range.commonAncestorContainer) && isWrappingNode(range.commonAncestorContainer.parentNode) && range.commonAncestorContainer.parentNode !== className) {
|
|
150
|
+
highlightContainerNodes(range.commonAncestorContainer, className, range, currentGroupId); // now the fun stuff: highlighting a mix of text and DOM nodes
|
|
151
|
+
} else {
|
|
152
|
+
rangeInfos = {
|
|
153
|
+
startNode: isElement(range.startContainer) ? range.startContainer.childNodes[range.startOffset] : range.startContainer,
|
|
154
|
+
startNodeContainer: range.startContainer,
|
|
155
|
+
startOffset: range.startOffset,
|
|
156
|
+
endNode: isElement(range.endContainer) && range.endOffset > 0 ? range.endContainer.childNodes[range.endOffset - 1] : range.endContainer,
|
|
157
|
+
endNodeContainer: range.endContainer,
|
|
158
|
+
endOffset: range.endOffset,
|
|
159
|
+
commonRange: range
|
|
160
|
+
};
|
|
161
|
+
isWrapping = false;
|
|
162
|
+
hasWrapped = false;
|
|
163
|
+
wrapTextNodesInRange(range.commonAncestorContainer, rangeInfos);
|
|
170
164
|
}
|
|
165
|
+
}
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
if (!keepEmptyNodes) {
|
|
168
|
+
// clean up the markup after wrapping...
|
|
169
|
+
range.commonAncestorContainer.normalize();
|
|
170
|
+
}
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
currentGroupId = 0;
|
|
173
|
+
isWrapping = false;
|
|
174
|
+
reindexGroups(getContainer());
|
|
175
|
+
mergeAdjacentWrappingNodes(getContainer());
|
|
176
|
+
unWrapEmptyHighlights();
|
|
177
|
+
});
|
|
183
178
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
179
|
+
if (options.clearOnClick) {
|
|
180
|
+
$(containerSelector + ' .' + className).off('click').on('click', clearSingleHighlight);
|
|
187
181
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a range is valid
|
|
185
|
+
* @param {Range} range
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
193
188
|
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
function isRangeValid(range) {
|
|
191
|
+
var rangeInContainer;
|
|
197
192
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
193
|
+
try {
|
|
194
|
+
rangeInContainer = $.contains(getContainer(), range.commonAncestorContainer) || getContainer().isSameNode(range.commonAncestorContainer);
|
|
195
|
+
return rangeInContainer && !range.collapsed;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return false;
|
|
204
198
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Core wrapping function. Traverse the DOM tree and highlight (= wraps) all text nodes within the given range.
|
|
202
|
+
* Recursive.
|
|
203
|
+
*
|
|
204
|
+
* @param {Node} rootNode - top of the node hierarchy in which text nodes will be searched
|
|
205
|
+
* @param {Object} rangeInfos
|
|
206
|
+
* @param {Node} rangeInfos.startNode - node on which the selection starts
|
|
207
|
+
* @param {Node} rangeInfos.startNodeContainer - container of the startNode, or the start node itself in case of text nodes
|
|
208
|
+
* @param {number} rangeInfos.startOffset - same as range.startOffset, but not read-only to allow override
|
|
209
|
+
* @param {Node} rangeInfos.endNode - node on which the selection ends
|
|
210
|
+
* @param {Node} rangeInfos.endNodeContainer - container of the endNode, or the end node itself in case of text nodes
|
|
211
|
+
* @param {number} rangeInfos.endOffset - same as range.endOffset, but not read-only to allow override
|
|
212
|
+
*/
|
|
218
213
|
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
215
|
+
function wrapTextNodesInRange(rootNode, rangeInfos) {
|
|
216
|
+
var childNodes = rootNode.childNodes;
|
|
217
|
+
var currentNode, i;
|
|
218
|
+
var splitDatas = [];
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
for (i = 0; i < childNodes.length; i++) {
|
|
221
|
+
if (hasWrapped) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
229
224
|
|
|
230
|
-
|
|
225
|
+
currentNode = childNodes[i];
|
|
231
226
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
227
|
+
if (isBlacklisted(currentNode)) {
|
|
228
|
+
if (isElement(currentNode)) {
|
|
229
|
+
//go deeper in case a descendant of the current blacklisted is whitelisted
|
|
230
|
+
wrapTextNodesInRange(currentNode, rangeInfos);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
var isCurrentNodeTextInsideOfAnotherHighlightingWrapper = isText(currentNode) && isWrappingNode(currentNode.parentNode) && currentNode.parentNode.className !== className;
|
|
239
234
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
235
|
+
if (isCurrentNodeTextInsideOfAnotherHighlightingWrapper) {
|
|
236
|
+
var internalRange = new Range();
|
|
237
|
+
internalRange.selectNodeContents(currentNode);
|
|
243
238
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
239
|
+
if (rangeInfos.startNode === currentNode) {
|
|
240
|
+
internalRange.setStart(currentNode, rangeInfos.startOffset);
|
|
241
|
+
}
|
|
247
242
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
if (rangeInfos.endNode === currentNode) {
|
|
244
|
+
internalRange.setEnd(currentNode, rangeInfos.endOffset);
|
|
245
|
+
}
|
|
251
246
|
|
|
252
|
-
|
|
247
|
+
var isNodeInRange = rangeInfos.commonRange.isPointInRange(currentNode, internalRange.endOffset); // Apply new highlighting color only for selected nodes
|
|
253
248
|
|
|
254
|
-
|
|
249
|
+
if (isNodeInRange) {
|
|
250
|
+
isWrapping = true;
|
|
251
|
+
highlightContainerNodes(currentNode, className, internalRange, currentGroupId);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// split current node in case the wrapping start/ends on a partially selected text node
|
|
255
|
+
if (currentNode.isSameNode(rangeInfos.startNode)) {
|
|
256
|
+
if (isText(rangeInfos.startNodeContainer) && rangeInfos.startOffset !== 0) {
|
|
257
|
+
// we defer the wrapping to the next iteration of the loop
|
|
258
|
+
//end of node should be highlighted
|
|
259
|
+
rangeInfos.startNode = currentNode.splitText(rangeInfos.startOffset);
|
|
260
|
+
rangeInfos.startOffset = 0;
|
|
261
|
+
splitDatas.push({
|
|
262
|
+
node: rangeInfos.startNode,
|
|
263
|
+
beforeWasSplit: true,
|
|
264
|
+
afterWasSplit: false
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
//whole node should be highlighted
|
|
255
268
|
isWrapping = true;
|
|
256
|
-
|
|
269
|
+
splitDatas.push({
|
|
270
|
+
node: currentNode,
|
|
271
|
+
beforeWasSplit: false,
|
|
272
|
+
afterWasSplit: false
|
|
273
|
+
});
|
|
257
274
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
rangeInfos.startOffset = 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (currentNode.isSameNode(rangeInfos.endNode) && isText(rangeInfos.endNodeContainer)) {
|
|
278
|
+
if (rangeInfos.endOffset !== 0) {
|
|
279
|
+
if (rangeInfos.endOffset < currentNode.textContent.length) {
|
|
280
|
+
//start of node should be highlighted
|
|
281
|
+
currentNode.splitText(rangeInfos.endOffset);
|
|
266
282
|
splitDatas.push({
|
|
267
|
-
node:
|
|
268
|
-
beforeWasSplit:
|
|
269
|
-
afterWasSplit:
|
|
283
|
+
node: currentNode,
|
|
284
|
+
beforeWasSplit: false,
|
|
285
|
+
afterWasSplit: true
|
|
270
286
|
});
|
|
271
287
|
} else {
|
|
272
288
|
//whole node should be highlighted
|
|
273
|
-
isWrapping = true;
|
|
274
289
|
splitDatas.push({
|
|
275
290
|
node: currentNode,
|
|
276
291
|
beforeWasSplit: false,
|
|
277
292
|
afterWasSplit: false
|
|
278
293
|
});
|
|
279
294
|
}
|
|
295
|
+
} else {
|
|
296
|
+
isWrapping = false;
|
|
280
297
|
}
|
|
298
|
+
} // wrap the current node...
|
|
281
299
|
|
|
282
|
-
if (currentNode.isSameNode(rangeInfos.endNode) && isText(rangeInfos.endNodeContainer)) {
|
|
283
|
-
if (rangeInfos.endOffset !== 0) {
|
|
284
|
-
if (rangeInfos.endOffset < currentNode.textContent.length) {
|
|
285
|
-
//start of node should be highlighted
|
|
286
|
-
currentNode.splitText(rangeInfos.endOffset);
|
|
287
|
-
splitDatas.push({
|
|
288
|
-
node: currentNode,
|
|
289
|
-
beforeWasSplit: false,
|
|
290
|
-
afterWasSplit: true
|
|
291
|
-
});
|
|
292
|
-
} else {
|
|
293
|
-
//whole node should be highlighted
|
|
294
|
-
splitDatas.push({
|
|
295
|
-
node: currentNode,
|
|
296
|
-
beforeWasSplit: false,
|
|
297
|
-
afterWasSplit: false
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
isWrapping = false;
|
|
302
|
-
}
|
|
303
|
-
} // wrap the current node...
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (isText(currentNode)) {
|
|
307
|
-
if (!keepEmptyNodes) {
|
|
308
|
-
wrapTextNode(currentNode, currentGroupId);
|
|
309
|
-
} else if (willHighlightNotBeEmptyAfterMerge(currentNode)) {
|
|
310
|
-
const wrapperNode = wrapTextNode(currentNode, currentGroupId);
|
|
311
300
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
301
|
+
if (isText(currentNode)) {
|
|
302
|
+
if (!keepEmptyNodes) {
|
|
303
|
+
wrapTextNode(currentNode, currentGroupId);
|
|
304
|
+
} else if (willHighlightNotBeEmptyAfterMerge(currentNode)) {
|
|
305
|
+
var wrapperNode = wrapTextNode(currentNode, currentGroupId);
|
|
317
306
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
307
|
+
if (wrapperNode) {
|
|
308
|
+
var splitData = splitDatas.find(function (d) {
|
|
309
|
+
return d.node === currentNode;
|
|
310
|
+
});
|
|
311
|
+
addSplitData(wrapperNode, splitData ? splitData.beforeWasSplit : false, splitData ? splitData.afterWasSplit : false);
|
|
322
312
|
}
|
|
313
|
+
} // ... or continue deeper in the node tree
|
|
314
|
+
|
|
315
|
+
} else if (isElement(currentNode)) {
|
|
316
|
+
//some selections end at the very start of the next node, we should end wrapping when we reach such node
|
|
317
|
+
if (!currentNode.isSameNode(rangeInfos.endNode) || rangeInfos.endOffset > 0) {
|
|
318
|
+
wrapTextNodesInRange(currentNode, rangeInfos);
|
|
323
319
|
}
|
|
324
320
|
}
|
|
325
|
-
}
|
|
321
|
+
}
|
|
322
|
+
} // end wrapping ?
|
|
326
323
|
|
|
327
324
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
325
|
+
if (currentNode.isSameNode(rangeInfos.endNode)) {
|
|
326
|
+
isWrapping = false;
|
|
327
|
+
hasWrapped = true;
|
|
328
|
+
break;
|
|
333
329
|
}
|
|
334
330
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Restructure content of the highlighted wrapper according to the selectedRange
|
|
334
|
+
* @param {Node} textNode
|
|
335
|
+
* @param {string} activeClass
|
|
336
|
+
* @param {Range} selectedRange
|
|
337
|
+
* @param {number} currentGroupId
|
|
338
|
+
*/
|
|
342
339
|
|
|
343
340
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
*/
|
|
391
|
-
|
|
392
|
-
if (isSelectionCoversNodeStart && isSelectionCoversNodeEnd) {
|
|
393
|
-
textNode.parentNode.className = activeClass;
|
|
394
|
-
} else if (isSelectionCoversNodeStart) {
|
|
395
|
-
textNode.splitText(selectedRange.endOffset);
|
|
396
|
-
wrapContainerChildNodes(container, 0, activeClass, currentGroupId);
|
|
397
|
-
} else if (isSelectionCoversNodeEnd) {
|
|
398
|
-
textNode.splitText(selectedRange.startOffset);
|
|
399
|
-
wrapContainerChildNodes(container, 1, activeClass, currentGroupId);
|
|
400
|
-
} else {
|
|
401
|
-
textNode.splitText(selectedRange.startOffset).splitText(selectedRange.endOffset);
|
|
402
|
-
wrapContainerChildNodes(container, 1, activeClass, currentGroupId);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Wraps all containers text nodes with highlighter element.
|
|
407
|
-
* The child node with index given by indexToWrapNode parameter will be wrap with class given by activeClass parameter
|
|
408
|
-
* @param {Element} container
|
|
409
|
-
* @param {number} indexToWrapNode
|
|
410
|
-
* @param {string} activeClass
|
|
411
|
-
* @param {number} currentGroupId
|
|
341
|
+
function highlightContainerNodes(textNode, activeClass, selectedRange, currentGroupId) {
|
|
342
|
+
var container = textNode.parentNode;
|
|
343
|
+
var range = new Range();
|
|
344
|
+
range.selectNodeContents(textNode);
|
|
345
|
+
var isSelectionCoversNodeStart = range.compareBoundaryPoints(Range.START_TO_START, selectedRange) === 0;
|
|
346
|
+
var isSelectionCoversNodeEnd = range.compareBoundaryPoints(Range.END_TO_END, selectedRange) === 0;
|
|
347
|
+
/*
|
|
348
|
+
There are 4 possible cases selected area is intersected with already highlighted element.
|
|
349
|
+
In examples below the border is represents the selection, "yellow" is class name of already highlighted
|
|
350
|
+
container, "red" is class name of currently active highlighter
|
|
351
|
+
**********************************************************************************************************
|
|
352
|
+
1. The container content is completely selected, so that we only have to change the highlighter class name
|
|
353
|
+
Input:
|
|
354
|
+
__________________________________________________
|
|
355
|
+
| |
|
|
356
|
+
|<span class="yellow"> Lorem ipsum dolor sit</span>|
|
|
357
|
+
|__________________________________________________|
|
|
358
|
+
Output:
|
|
359
|
+
<span class="red"> Lorem ipsum dolor sit</span>
|
|
360
|
+
**********************************************************************************************************
|
|
361
|
+
2. The container content is partially selected from the begging.
|
|
362
|
+
Input:
|
|
363
|
+
______________________________
|
|
364
|
+
| |
|
|
365
|
+
|<span class="yellow"> Lorem ip|sum dolor sit</span>
|
|
366
|
+
|______________________________|
|
|
367
|
+
Output:
|
|
368
|
+
<span class="red"> Lorem ip</span><span class="yellow">sum dolor sit</span>
|
|
369
|
+
**********************************************************************************************************
|
|
370
|
+
3. The container content is partially selected at the end.
|
|
371
|
+
Input:
|
|
372
|
+
____________________
|
|
373
|
+
| |
|
|
374
|
+
<span class="yellow"> Lorem ip|sum dolor sit</span>|
|
|
375
|
+
|____________________|
|
|
376
|
+
Output:
|
|
377
|
+
<span class="yellow"> Lorem ip</span><span class="red">sum dolor sit</span>
|
|
378
|
+
**********************************************************************************************************
|
|
379
|
+
4. The container content is partially selected in the middle.
|
|
380
|
+
Input:
|
|
381
|
+
___________
|
|
382
|
+
| |
|
|
383
|
+
<span class="yellow"> Lorem |ipsum dolor| sit</span>
|
|
384
|
+
|___________|
|
|
385
|
+
Output:
|
|
386
|
+
<span class="yellow"> Lorem </span><span class="red">ipsum dolor</span><span class="yellow"> sit</span>
|
|
412
387
|
*/
|
|
413
388
|
|
|
389
|
+
if (isSelectionCoversNodeStart && isSelectionCoversNodeEnd) {
|
|
390
|
+
textNode.parentNode.className = activeClass;
|
|
391
|
+
} else if (isSelectionCoversNodeStart) {
|
|
392
|
+
textNode.splitText(selectedRange.endOffset);
|
|
393
|
+
wrapContainerChildNodes(container, 0, activeClass, currentGroupId);
|
|
394
|
+
} else if (isSelectionCoversNodeEnd) {
|
|
395
|
+
textNode.splitText(selectedRange.startOffset);
|
|
396
|
+
wrapContainerChildNodes(container, 1, activeClass, currentGroupId);
|
|
397
|
+
} else {
|
|
398
|
+
textNode.splitText(selectedRange.startOffset).splitText(selectedRange.endOffset);
|
|
399
|
+
wrapContainerChildNodes(container, 1, activeClass, currentGroupId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Wraps all containers text nodes with highlighter element.
|
|
404
|
+
* The child node with index given by indexToWrapNode parameter will be wrap with class given by activeClass parameter
|
|
405
|
+
* @param {Element} container
|
|
406
|
+
* @param {number} indexToWrapNode
|
|
407
|
+
* @param {string} activeClass
|
|
408
|
+
* @param {number} currentGroupId
|
|
409
|
+
*/
|
|
414
410
|
|
|
415
|
-
function wrapContainerChildNodes(container, indexToWrapNode, activeClass, currentGroupId) {
|
|
416
|
-
const containerClass = container.className;
|
|
417
|
-
const fragment = new DocumentFragment();
|
|
418
|
-
const childNodesLength = container.childNodes.length;
|
|
419
|
-
container.childNodes.forEach((node, index) => {
|
|
420
|
-
var wrapperNode;
|
|
421
|
-
|
|
422
|
-
if (index === indexToWrapNode) {
|
|
423
|
-
wrapperNode = wrapNode(node.cloneNode(), activeClass, currentGroupId);
|
|
424
|
-
} else {
|
|
425
|
-
wrapperNode = wrapNode(node.cloneNode(), containerClass, currentGroupId);
|
|
426
|
-
}
|
|
427
411
|
|
|
428
|
-
|
|
412
|
+
function wrapContainerChildNodes(container, indexToWrapNode, activeClass, currentGroupId) {
|
|
413
|
+
var containerClass = container.className;
|
|
414
|
+
var fragment = new DocumentFragment();
|
|
415
|
+
var childNodesLength = container.childNodes.length;
|
|
416
|
+
container.childNodes.forEach(function (node, index) {
|
|
417
|
+
var wrapperNode;
|
|
429
418
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* wraps a text node into the highlight span
|
|
438
|
-
* @param {Node} node - the node to wrap
|
|
439
|
-
* @param {number} groupId - the highlight group
|
|
440
|
-
* @returns {Node|null} wrapper node, if it was created
|
|
441
|
-
*/
|
|
419
|
+
if (index === indexToWrapNode) {
|
|
420
|
+
wrapperNode = wrapNode(node.cloneNode(), activeClass, currentGroupId);
|
|
421
|
+
} else {
|
|
422
|
+
wrapperNode = wrapNode(node.cloneNode(), containerClass, currentGroupId);
|
|
423
|
+
}
|
|
442
424
|
|
|
425
|
+
fragment.appendChild(wrapperNode);
|
|
443
426
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
$(node).wrap(getWrapper(groupId));
|
|
447
|
-
return node.parentNode;
|
|
427
|
+
if (keepEmptyNodes) {
|
|
428
|
+
addSplitData(wrapperNode, index === 0 ? container.dataset.beforeWasSplit : true, index === childNodesLength - 1 ? container.dataset.afterWasSplit : true);
|
|
448
429
|
}
|
|
430
|
+
});
|
|
431
|
+
container.replaceWith(fragment);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* wraps a text node into the highlight span
|
|
435
|
+
* @param {Node} node - the node to wrap
|
|
436
|
+
* @param {number} groupId - the highlight group
|
|
437
|
+
* @returns {Node|null} wrapper node, if it was created
|
|
438
|
+
*/
|
|
449
439
|
|
|
450
|
-
|
|
440
|
+
|
|
441
|
+
function wrapTextNode(node, groupId) {
|
|
442
|
+
if (isWrapping && !isWrappingNode(node.parentNode) && isWrappable(node)) {
|
|
443
|
+
$(node).wrap(getWrapper(groupId));
|
|
444
|
+
return node.parentNode;
|
|
451
445
|
}
|
|
452
|
-
/**
|
|
453
|
-
* We need to re-index the groups after a user highlight: either to merge groups or to resolve inconsistencies
|
|
454
|
-
* Recursive.
|
|
455
|
-
*
|
|
456
|
-
* @param {Node} rootNode
|
|
457
|
-
*/
|
|
458
446
|
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* We need to re-index the groups after a user highlight: either to merge groups or to resolve inconsistencies
|
|
451
|
+
* Recursive.
|
|
452
|
+
*
|
|
453
|
+
* @param {Node} rootNode
|
|
454
|
+
*/
|
|
459
455
|
|
|
460
|
-
function reindexGroups(rootNode) {
|
|
461
|
-
if (!rootNode) {
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
456
|
|
|
465
|
-
|
|
466
|
-
|
|
457
|
+
function reindexGroups(rootNode) {
|
|
458
|
+
if (!rootNode) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
467
461
|
|
|
468
|
-
|
|
469
|
-
|
|
462
|
+
var childNodes = rootNode.childNodes;
|
|
463
|
+
var i, currentNode, parent;
|
|
470
464
|
|
|
471
|
-
|
|
472
|
-
|
|
465
|
+
for (i = 0; i < childNodes.length; i++) {
|
|
466
|
+
currentNode = childNodes[i];
|
|
473
467
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
currentGroupId++;
|
|
477
|
-
}
|
|
468
|
+
if (isWrappable(currentNode)) {
|
|
469
|
+
parent = currentNode.parentNode;
|
|
478
470
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
isWrapping = false;
|
|
471
|
+
if (isWrappingNode(parent)) {
|
|
472
|
+
if (isWrapping === false) {
|
|
473
|
+
currentGroupId++;
|
|
483
474
|
}
|
|
484
|
-
|
|
485
|
-
|
|
475
|
+
|
|
476
|
+
isWrapping = true;
|
|
477
|
+
parent.setAttribute(GROUP_ATTR, currentGroupId); // set the new group Id
|
|
478
|
+
} else {
|
|
479
|
+
isWrapping = false;
|
|
486
480
|
}
|
|
481
|
+
} else if (isElement(currentNode)) {
|
|
482
|
+
reindexGroups(currentNode);
|
|
487
483
|
}
|
|
488
484
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Some highlights may result in having adjacent wrapping nodes. We remove them here to get a cleaner markup.
|
|
488
|
+
*
|
|
489
|
+
* @param {Node} rootNode
|
|
490
|
+
*/
|
|
494
491
|
|
|
495
492
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
493
|
+
function mergeAdjacentWrappingNodes(rootNode) {
|
|
494
|
+
if (!rootNode) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
500
497
|
|
|
501
|
-
|
|
502
|
-
|
|
498
|
+
var childNodes = rootNode.childNodes;
|
|
499
|
+
var i, currentNode;
|
|
503
500
|
|
|
504
|
-
|
|
505
|
-
|
|
501
|
+
for (i = 0; i < childNodes.length; i++) {
|
|
502
|
+
currentNode = childNodes[i];
|
|
506
503
|
|
|
507
|
-
|
|
504
|
+
if (isWrappingNode(currentNode)) {
|
|
505
|
+
if (keepEmptyNodes) {
|
|
506
|
+
currentNode.normalize();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
while (isWrappingNode(currentNode.nextSibling) && currentNode.className === currentNode.nextSibling.className) {
|
|
508
510
|
if (keepEmptyNodes) {
|
|
509
|
-
currentNode.normalize();
|
|
511
|
+
currentNode.nextSibling.normalize();
|
|
510
512
|
}
|
|
511
513
|
|
|
512
|
-
|
|
513
|
-
if (keepEmptyNodes) {
|
|
514
|
-
currentNode.nextSibling.normalize();
|
|
515
|
-
}
|
|
514
|
+
currentNode.firstChild.textContent += currentNode.nextSibling.firstChild.textContent;
|
|
516
515
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (keepEmptyNodes) {
|
|
520
|
-
addSplitData(currentNode, currentNode.dataset.beforeWasSplit, currentNode.nextSibling.dataset.afterWasSplit);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
currentNode.parentNode.removeChild(currentNode.nextSibling);
|
|
516
|
+
if (keepEmptyNodes) {
|
|
517
|
+
addSplitData(currentNode, currentNode.dataset.beforeWasSplit, currentNode.nextSibling.dataset.afterWasSplit);
|
|
524
518
|
}
|
|
525
|
-
|
|
526
|
-
|
|
519
|
+
|
|
520
|
+
currentNode.parentNode.removeChild(currentNode.nextSibling);
|
|
527
521
|
}
|
|
522
|
+
} else if (isElement(currentNode)) {
|
|
523
|
+
mergeAdjacentWrappingNodes(currentNode);
|
|
528
524
|
}
|
|
529
525
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Unwraps highlighted nodes with a line break or with an empty content
|
|
529
|
+
*/
|
|
533
530
|
|
|
534
531
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
532
|
+
function unWrapEmptyHighlights() {
|
|
533
|
+
getHighlightedNodes().each(function (index, node) {
|
|
534
|
+
var nodeContent = node.textContent;
|
|
538
535
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
536
|
+
if (nodeContent.trim().length === 0) {
|
|
537
|
+
if (nodeContent.length === 0 || /\r|\n/.exec(nodeContent)) {
|
|
538
|
+
clearSingleHighlight({
|
|
539
|
+
target: node
|
|
540
|
+
});
|
|
545
541
|
}
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Check condition to avoid the work of `unwrapEmptyHighlights` ahead of time, before `mergeAdjacentNodes` runs,
|
|
550
|
-
* because in `keepEmptyNodes` case we do not want to add nodes to dom unless necessary.
|
|
551
|
-
* Also be more strict and don't allow to select nodes with spaces only, because they may appear in unexpected places in markup
|
|
552
|
-
* (here it's not exactly same as `unwrapEmptyHighlights`).
|
|
553
|
-
* @param {Node} node - node which will be wrapped (highlighted)
|
|
554
|
-
* @returns {Boolean}
|
|
555
|
-
*/
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
function willHighlightNotBeEmptyAfterMerge(node) {
|
|
559
|
-
if (!node.textContent.length) {
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (node.textContent.trim().length) {
|
|
564
|
-
return true;
|
|
565
542
|
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Check condition to avoid the work of `unwrapEmptyHighlights` ahead of time, before `mergeAdjacentNodes` runs,
|
|
547
|
+
* because in `keepEmptyNodes` case we do not want to add nodes to dom unless necessary.
|
|
548
|
+
* Also be more strict and don't allow to select nodes with spaces only, because they may appear in unexpected places in markup
|
|
549
|
+
* (here it's not exactly same as `unwrapEmptyHighlights`).
|
|
550
|
+
* @param {Node} node - node which will be wrapped (highlighted)
|
|
551
|
+
* @returns {Boolean}
|
|
552
|
+
*/
|
|
566
553
|
|
|
567
|
-
const prevNode = node.previousSibling;
|
|
568
|
-
const canWrapperBeMergedWithPreviousSibling = prevNode && isWrappingNode(prevNode) && prevNode.className === className;
|
|
569
554
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
555
|
+
function willHighlightNotBeEmptyAfterMerge(node) {
|
|
556
|
+
if (!node.textContent.length) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
573
559
|
|
|
574
|
-
|
|
575
|
-
|
|
560
|
+
if (node.textContent.trim().length) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
576
563
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
564
|
+
var prevNode = node.previousSibling;
|
|
565
|
+
var canWrapperBeMergedWithPreviousSibling = prevNode && isWrappingNode(prevNode) && prevNode.className === className;
|
|
580
566
|
|
|
581
|
-
|
|
567
|
+
if (canWrapperBeMergedWithPreviousSibling) {
|
|
568
|
+
return true;
|
|
582
569
|
}
|
|
583
|
-
/**
|
|
584
|
-
* `range.surroundContents` can create empty text nodes,
|
|
585
|
-
* which will cause trouble in `mergeAdjacentNodes` later (in `keepEmptyNodes` case).
|
|
586
|
-
* This method surrounds range, then removes those nodes
|
|
587
|
-
* @param {Range} range
|
|
588
|
-
* @param {Node} wrapperNode
|
|
589
|
-
*/
|
|
590
570
|
|
|
571
|
+
var nextNode = node.nextSibling;
|
|
572
|
+
var canWrapperBeMergedWithNextSibling = nextNode && isWrappingNode(nextNode) && nextNode.className === className;
|
|
591
573
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const containerNextSibling = range.commonAncestorContainer.nextSibling;
|
|
595
|
-
range.surroundContents(wrapperNode);
|
|
596
|
-
removeEmptyTextNodeIfDifferent(wrapperNode.previousSibling, containerPreviousSibling);
|
|
597
|
-
removeEmptyTextNodeIfDifferent(wrapperNode.nextSibling, containerNextSibling);
|
|
574
|
+
if (canWrapperBeMergedWithNextSibling) {
|
|
575
|
+
return true;
|
|
598
576
|
}
|
|
599
|
-
/**
|
|
600
|
-
* Remove `node`, if it's an empty text node and is *not* the same node as `nodeToCompare`
|
|
601
|
-
* @param {Node} node
|
|
602
|
-
* @param {Node} nodeToCompare
|
|
603
|
-
*/
|
|
604
577
|
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* `range.surroundContents` can create empty text nodes,
|
|
582
|
+
* which will cause trouble in `mergeAdjacentNodes` later (in `keepEmptyNodes` case).
|
|
583
|
+
* This method surrounds range, then removes those nodes
|
|
584
|
+
* @param {Range} range
|
|
585
|
+
* @param {Node} wrapperNode
|
|
586
|
+
*/
|
|
605
587
|
|
|
606
|
-
function removeEmptyTextNodeIfDifferent(node, nodeToCompare) {
|
|
607
|
-
if (node && node !== nodeToCompare && isText(node) && node.textContent.length === 0) {
|
|
608
|
-
node.remove();
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Remove all wrapping nodes from markup
|
|
613
|
-
*/
|
|
614
588
|
|
|
589
|
+
function rangeSurroundContentsNoEmptyNodes(range, wrapperNode) {
|
|
590
|
+
var containerPreviousSibling = range.commonAncestorContainer.previousSibling;
|
|
591
|
+
var containerNextSibling = range.commonAncestorContainer.nextSibling;
|
|
592
|
+
range.surroundContents(wrapperNode);
|
|
593
|
+
removeEmptyTextNodeIfDifferent(wrapperNode.previousSibling, containerPreviousSibling);
|
|
594
|
+
removeEmptyTextNodeIfDifferent(wrapperNode.nextSibling, containerNextSibling);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Remove `node`, if it's an empty text node and is *not* the same node as `nodeToCompare`
|
|
598
|
+
* @param {Node} node
|
|
599
|
+
* @param {Node} nodeToCompare
|
|
600
|
+
*/
|
|
615
601
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
$wrapped.replaceWith($wrapped.text());
|
|
621
|
-
} else {
|
|
622
|
-
clearSingleHighlight({
|
|
623
|
-
target: elem
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
});
|
|
602
|
+
|
|
603
|
+
function removeEmptyTextNodeIfDifferent(node, nodeToCompare) {
|
|
604
|
+
if (node && node !== nodeToCompare && isText(node) && node.textContent.length === 0) {
|
|
605
|
+
node.remove();
|
|
627
606
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Remove all wrapping nodes from markup
|
|
610
|
+
*/
|
|
631
611
|
|
|
632
612
|
|
|
633
|
-
|
|
613
|
+
function clearHighlights() {
|
|
614
|
+
getHighlightedNodes().each(function (i, elem) {
|
|
634
615
|
if (!keepEmptyNodes) {
|
|
635
|
-
|
|
636
|
-
|
|
616
|
+
var $wrapped = $(this);
|
|
617
|
+
$wrapped.replaceWith($wrapped.text());
|
|
618
|
+
} else {
|
|
619
|
+
clearSingleHighlight({
|
|
620
|
+
target: elem
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Remove unwrap dom node
|
|
627
|
+
*/
|
|
637
628
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
629
|
+
|
|
630
|
+
function clearSingleHighlight(e) {
|
|
631
|
+
if (!keepEmptyNodes) {
|
|
632
|
+
var $wrapped = $(e.target);
|
|
633
|
+
var text = $wrapped.text(); // NOTE: JQuery replaceWith is not working with empty string https://bugs.jquery.com/ticket/13401
|
|
634
|
+
|
|
635
|
+
if (text === '') {
|
|
636
|
+
$wrapped.remove();
|
|
643
637
|
} else {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
nextNode.textContent = nodeToRemoveText + nextNode.textContent;
|
|
664
|
-
nodeToRemove.remove();
|
|
665
|
-
} else if (nodeToRemoveText) {
|
|
666
|
-
//keep text in a separate text node
|
|
667
|
-
nodeToRemove.replaceWith(document.createTextNode(nodeToRemoveText));
|
|
668
|
-
} else {
|
|
669
|
-
//text is empty, just remove it
|
|
670
|
-
nodeToRemove.remove();
|
|
638
|
+
$wrapped.replaceWith(text);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
var nodeToRemove = e.target;
|
|
642
|
+
var nodeToRemoveText = nodeToRemove.textContent;
|
|
643
|
+
var beforeWasSplit = nodeToRemove.dataset.beforeWasSplit === 'true';
|
|
644
|
+
var afterWasSplit = nodeToRemove.dataset.afterWasSplit === 'true';
|
|
645
|
+
var prevNode = nodeToRemove.previousSibling;
|
|
646
|
+
var nextNode = nodeToRemove.nextSibling;
|
|
647
|
+
|
|
648
|
+
if (beforeWasSplit && prevNode && isText(prevNode) && prevNode.textContent) {
|
|
649
|
+
//append text to previous sibling
|
|
650
|
+
prevNode.textContent += nodeToRemoveText;
|
|
651
|
+
nodeToRemove.remove();
|
|
652
|
+
|
|
653
|
+
if (afterWasSplit && prevNode.nextSibling && isText(prevNode.nextSibling) && prevNode.nextSibling.textContent) {
|
|
654
|
+
//merge it with next sibling
|
|
655
|
+
prevNode.textContent += prevNode.nextSibling.textContent;
|
|
656
|
+
prevNode.nextSibling.remove();
|
|
671
657
|
}
|
|
658
|
+
} else if (afterWasSplit && nextNode && isText(nextNode) && nextNode.textContent) {
|
|
659
|
+
//append text to next sibling
|
|
660
|
+
nextNode.textContent = nodeToRemoveText + nextNode.textContent;
|
|
661
|
+
nodeToRemove.remove();
|
|
662
|
+
} else if (nodeToRemoveText) {
|
|
663
|
+
//keep text in a separate text node
|
|
664
|
+
nodeToRemove.replaceWith(document.createTextNode(nodeToRemoveText));
|
|
665
|
+
} else {
|
|
666
|
+
//text is empty, just remove it
|
|
667
|
+
nodeToRemove.remove();
|
|
672
668
|
}
|
|
673
669
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Index-related functions:
|
|
673
|
+
* ========================
|
|
674
|
+
* To allow saving and restoring highlights on an equivalent, but different, DOM tree (for example if the markup is deleted and re-created)
|
|
675
|
+
* we build an index containing the status of each text node:
|
|
676
|
+
* - not highlighted
|
|
677
|
+
* - fully highlighted
|
|
678
|
+
* - partially highlighted (= with inline ranges)
|
|
679
|
+
*
|
|
680
|
+
* This index will be used to restore a selection on the new DOM tree
|
|
681
|
+
*/
|
|
685
682
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
683
|
+
/**
|
|
684
|
+
* Bootstrap the process of building the highlight index
|
|
685
|
+
* @returns {Object[]|BuildModelResultKeepEmpty|null}
|
|
686
|
+
*/
|
|
690
687
|
|
|
691
688
|
|
|
692
|
-
|
|
693
|
-
|
|
689
|
+
function getHighlightIndex() {
|
|
690
|
+
var rootNode = getContainer();
|
|
694
691
|
|
|
695
|
-
|
|
696
|
-
|
|
692
|
+
if (!keepEmptyNodes) {
|
|
693
|
+
var highlightIndex = [];
|
|
697
694
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
695
|
+
if (rootNode) {
|
|
696
|
+
rootNode.normalize();
|
|
697
|
+
textNodesIndex = 0;
|
|
698
|
+
buildHighlightIndex(rootNode, highlightIndex);
|
|
699
|
+
}
|
|
703
700
|
|
|
704
|
-
|
|
701
|
+
return highlightIndex;
|
|
702
|
+
} else {
|
|
703
|
+
if (rootNode) {
|
|
704
|
+
return buildHighlightModelKeepEmpty(rootNode);
|
|
705
705
|
} else {
|
|
706
|
-
|
|
707
|
-
return buildHighlightModelKeepEmpty(rootNode);
|
|
708
|
-
} else {
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
706
|
+
return null;
|
|
711
707
|
}
|
|
712
708
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
function buildHighlightIndex(rootNode, highlightIndex) {
|
|
721
|
-
var childNodes = rootNode.childNodes;
|
|
722
|
-
var i, currentNode;
|
|
723
|
-
var nodeInfos, inlineRange, inlineOffset, nodesToSkip;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Traverse the DOM tree to create the text Nodes index. Recursive.
|
|
712
|
+
* @param {Node} rootNode
|
|
713
|
+
* @param {Object[]} highlightIndex
|
|
714
|
+
*/
|
|
724
715
|
|
|
725
|
-
for (i = 0; i < childNodes.length; i++) {
|
|
726
|
-
currentNode = childNodes[i]; // Skip blacklisted nodes
|
|
727
716
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
717
|
+
function buildHighlightIndex(rootNode, highlightIndex) {
|
|
718
|
+
var childNodes = rootNode.childNodes;
|
|
719
|
+
var i, currentNode;
|
|
720
|
+
var nodeInfos, inlineRange, inlineOffset, nodesToSkip;
|
|
721
|
+
|
|
722
|
+
for (i = 0; i < childNodes.length; i++) {
|
|
723
|
+
currentNode = childNodes[i]; // Skip blacklisted nodes
|
|
724
|
+
|
|
725
|
+
if (isBlacklisted(currentNode)) {
|
|
726
|
+
continue;
|
|
727
|
+
} // A simple node not highlighted and isolated (= not followed by an wrapped text)
|
|
728
|
+
else if (isWrappable(currentNode) && !isWrappingNode(currentNode.nextSibling)) {
|
|
729
|
+
highlightIndex[textNodesIndex] = {
|
|
730
|
+
highlighted: false
|
|
731
|
+
};
|
|
732
|
+
textNodesIndex++; // an isolated node (= not followed by a highlight table text) with its whole content highlighted
|
|
733
|
+
} else if (isWrappingNode(currentNode) && !isText(currentNode.nextSibling) && (!isWrappingNode(currentNode.nextSibling) || currentNode.className === currentNode.nextSibling.className)) {
|
|
734
|
+
highlightIndex[textNodesIndex] = {
|
|
735
|
+
highlighted: true,
|
|
736
|
+
groupId: currentNode.getAttribute(GROUP_ATTR),
|
|
737
|
+
c: getColorByClassName(currentNode.className)
|
|
738
|
+
};
|
|
739
|
+
textNodesIndex++; // less straightforward: a succession of (at least) 1 wrapping node with 1 wrappable text node, in either order, and possibly more
|
|
740
|
+
// the trick is to create a unique text node on which we will be able to re-apply multiple partial highlights
|
|
741
|
+
// for this, we use 'inlineRanges'
|
|
742
|
+
} else if (isHotNode(currentNode)) {
|
|
743
|
+
nodeInfos = {
|
|
744
|
+
highlighted: true,
|
|
745
|
+
inlineRanges: []
|
|
746
|
+
};
|
|
747
|
+
nodesToSkip = -1;
|
|
748
|
+
inlineOffset = 0;
|
|
749
|
+
|
|
750
|
+
while (currentNode) {
|
|
751
|
+
if (isWrappingNode(currentNode)) {
|
|
752
|
+
inlineRange = {
|
|
753
|
+
groupId: currentNode.getAttribute(GROUP_ATTR),
|
|
754
|
+
c: getColorByClassName(currentNode.className)
|
|
755
|
+
};
|
|
763
756
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
757
|
+
if (isText(currentNode.previousSibling) || isWrappingNode(currentNode.previousSibling)) {
|
|
758
|
+
inlineRange.startOffset = inlineOffset;
|
|
759
|
+
}
|
|
767
760
|
|
|
768
|
-
|
|
761
|
+
if (isText(currentNode.nextSibling) || isWrappingNode(currentNode.nextSibling)) {
|
|
762
|
+
inlineRange.endOffset = inlineOffset + currentNode.textContent.length;
|
|
769
763
|
}
|
|
770
764
|
|
|
771
|
-
|
|
772
|
-
currentNode = isHotNode(currentNode.nextSibling) || isText(currentNode.nextSibling) ? currentNode.nextSibling : null;
|
|
773
|
-
nodesToSkip++;
|
|
765
|
+
nodeInfos.inlineRanges.push(inlineRange);
|
|
774
766
|
}
|
|
775
767
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
textNodesIndex++; // go deeper in the node tree...
|
|
780
|
-
} else if (isElement(currentNode)) {
|
|
781
|
-
buildHighlightIndex(currentNode, highlightIndex);
|
|
768
|
+
inlineOffset += currentNode.textContent.length;
|
|
769
|
+
currentNode = isHotNode(currentNode.nextSibling) || isText(currentNode.nextSibling) ? currentNode.nextSibling : null;
|
|
770
|
+
nodesToSkip++;
|
|
782
771
|
}
|
|
772
|
+
|
|
773
|
+
i += nodesToSkip; // we increase the loop counter to avoid looping over the nodes that we just analyzed
|
|
774
|
+
|
|
775
|
+
highlightIndex[textNodesIndex] = nodeInfos;
|
|
776
|
+
textNodesIndex++; // go deeper in the node tree...
|
|
777
|
+
} else if (isElement(currentNode)) {
|
|
778
|
+
buildHighlightIndex(currentNode, highlightIndex);
|
|
783
779
|
}
|
|
784
780
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* @typedef HighlightEntryKeepEmpty
|
|
784
|
+
* @property {String} groupId
|
|
785
|
+
* @property {String} c - color
|
|
786
|
+
* @property {Number} offsetBefore
|
|
787
|
+
* @property {Number} textLength
|
|
788
|
+
* @property {String} beforeWasSplit
|
|
789
|
+
* @property {String} afterWasSplit
|
|
790
|
+
* @property {Array<Number>} path - on each level from root container to highlight, index among siblings
|
|
791
|
+
*/
|
|
795
792
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
793
|
+
/**
|
|
794
|
+
* @typedef BuildModelResultKeepEmpty
|
|
795
|
+
* @property {HighlightEntryKeepEmpty[]} highlightModel
|
|
796
|
+
* @property {NodeList} wrapperNodes
|
|
797
|
+
*/
|
|
801
798
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
799
|
+
/**
|
|
800
|
+
* For `keepEmptyNodes` option, creates data model of highlights.
|
|
801
|
+
* Additionally returns array of highlight nodes. Traverses DOM tree.
|
|
802
|
+
* @param {Node} rootNode
|
|
803
|
+
* @returns {BuildModelResultKeepEmpty|null} result
|
|
804
|
+
*/
|
|
808
805
|
|
|
809
806
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
807
|
+
function buildHighlightModelKeepEmpty(rootNode) {
|
|
808
|
+
var classNames = options.colors ? Object.values(options.colors) : [className];
|
|
809
|
+
var wrapperNodesSelector = classNames.map(function (cls) {
|
|
810
|
+
return containerSelector + ' .' + cls;
|
|
811
|
+
}).join(', ');
|
|
812
|
+
var wrapperNodes = Array.from(document.querySelectorAll(wrapperNodesSelector)).filter(function (node) {
|
|
813
|
+
return !isBlacklisted(node);
|
|
814
|
+
});
|
|
814
815
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
816
|
+
if (!wrapperNodes.length) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
818
819
|
|
|
819
|
-
|
|
820
|
-
|
|
820
|
+
var highlightModel = [];
|
|
821
|
+
var indexCache = new Map();
|
|
821
822
|
|
|
822
|
-
|
|
823
|
-
|
|
823
|
+
for (var k = 0; k < wrapperNodes.length; k++) {
|
|
824
|
+
var wrapperNode = wrapperNodes[k]; //get info about highlight itself
|
|
824
825
|
|
|
825
|
-
|
|
826
|
-
|
|
826
|
+
var offsetBefore = 0;
|
|
827
|
+
var prevNode = wrapperNode.previousSibling;
|
|
827
828
|
|
|
828
|
-
|
|
829
|
-
|
|
829
|
+
if (prevNode && isText(prevNode)) {
|
|
830
|
+
var beforeWasSplit = wrapperNode.dataset.beforeWasSplit === 'true';
|
|
830
831
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
832
|
+
if (beforeWasSplit) {
|
|
833
|
+
offsetBefore = prevNode.textContent.length;
|
|
834
834
|
}
|
|
835
|
+
}
|
|
835
836
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
837
|
+
var highlightData = {
|
|
838
|
+
groupId: wrapperNode.getAttribute(GROUP_ATTR),
|
|
839
|
+
c: getColorByClassName(wrapperNode.className),
|
|
840
|
+
offsetBefore: offsetBefore,
|
|
841
|
+
textLength: wrapperNode.textContent.length,
|
|
842
|
+
beforeWasSplit: wrapperNode.dataset.beforeWasSplit,
|
|
843
|
+
afterWasSplit: wrapperNode.dataset.afterWasSplit,
|
|
844
|
+
path: []
|
|
845
|
+
}; //get info about its position in the tree: path through all parents from rootNode to highlight
|
|
846
|
+
|
|
847
|
+
var currentNode = wrapperNode;
|
|
848
|
+
|
|
849
|
+
while (currentNode && currentNode !== rootNode) {
|
|
850
|
+
var indexInModel = indexCache.get(currentNode);
|
|
851
|
+
|
|
852
|
+
if (!indexInModel && indexInModel !== 0) {
|
|
853
|
+
//should be more reliable to ignore empty nodes when indexing
|
|
854
|
+
var childNodes = Array.from(currentNode.parentNode.childNodes).filter(function (node) {
|
|
855
|
+
return !(isText(node) && !node.textContent.length);
|
|
856
|
+
}); //index among its non-empty siblings
|
|
857
|
+
|
|
858
|
+
indexInModel = childNodes.indexOf(currentNode);
|
|
859
|
+
indexCache.set(currentNode, indexInModel);
|
|
860
|
+
}
|
|
845
861
|
|
|
846
|
-
|
|
862
|
+
highlightData.path.unshift(indexInModel);
|
|
863
|
+
currentNode = currentNode.parentNode;
|
|
864
|
+
} //add info about highlight and its position to model
|
|
847
865
|
|
|
848
|
-
while (currentNode && currentNode !== rootNode) {
|
|
849
|
-
let indexInModel = indexCache.get(currentNode);
|
|
850
866
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const childNodes = Array.from(currentNode.parentNode.childNodes).filter(node => !(isText(node) && !node.textContent.length)); //index among its non-empty siblings
|
|
867
|
+
highlightModel.push(highlightData);
|
|
868
|
+
}
|
|
854
869
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
870
|
+
return {
|
|
871
|
+
highlightModel: highlightModel,
|
|
872
|
+
wrapperNodes: wrapperNodes
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Bootstrap the process of restoring the highlights from an index
|
|
877
|
+
* @param {Object[]|HighlightEntryKeepEmpty[]|null} highlightIndex
|
|
878
|
+
*/
|
|
858
879
|
|
|
859
|
-
highlightData.path.unshift(indexInModel);
|
|
860
|
-
currentNode = currentNode.parentNode;
|
|
861
|
-
} //add info about highlight and its position to model
|
|
862
880
|
|
|
881
|
+
function highlightFromIndex(highlightIndex) {
|
|
882
|
+
var rootNode = getContainer();
|
|
863
883
|
|
|
864
|
-
|
|
884
|
+
if (rootNode) {
|
|
885
|
+
if (!keepEmptyNodes) {
|
|
886
|
+
rootNode.normalize();
|
|
887
|
+
textNodesIndex = 0;
|
|
888
|
+
restoreHighlight(rootNode, highlightIndex);
|
|
889
|
+
} else {
|
|
890
|
+
restoreHighlightKeepEmpty(rootNode, highlightIndex);
|
|
865
891
|
}
|
|
866
|
-
|
|
867
|
-
return {
|
|
868
|
-
highlightModel,
|
|
869
|
-
wrapperNodes
|
|
870
|
-
};
|
|
871
892
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Traverse the DOM tree to wraps the text nodes according to the highlight index. Recursive.
|
|
896
|
+
* @param {Node} rootNode
|
|
897
|
+
* @param {Object[]} highlightIndex
|
|
898
|
+
*/
|
|
876
899
|
|
|
877
900
|
|
|
878
|
-
|
|
879
|
-
|
|
901
|
+
function restoreHighlight(rootNode, highlightIndex) {
|
|
902
|
+
var childNodes = rootNode.childNodes;
|
|
903
|
+
var i, currentNode, parent;
|
|
904
|
+
var nodeInfos, nodesToSkip, range, initialChildCount;
|
|
880
905
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
rootNode.normalize();
|
|
884
|
-
textNodesIndex = 0;
|
|
885
|
-
restoreHighlight(rootNode, highlightIndex);
|
|
886
|
-
} else {
|
|
887
|
-
restoreHighlightKeepEmpty(rootNode, highlightIndex);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Traverse the DOM tree to wraps the text nodes according to the highlight index. Recursive.
|
|
893
|
-
* @param {Node} rootNode
|
|
894
|
-
* @param {Object[]} highlightIndex
|
|
895
|
-
*/
|
|
906
|
+
for (i = 0; i < childNodes.length; i++) {
|
|
907
|
+
currentNode = childNodes[i];
|
|
896
908
|
|
|
909
|
+
if (isBlacklisted(currentNode)) {
|
|
910
|
+
continue;
|
|
911
|
+
} else if (isWrappable(currentNode)) {
|
|
912
|
+
parent = currentNode.parentNode;
|
|
913
|
+
initialChildCount = parent.childNodes.length;
|
|
914
|
+
nodeInfos = highlightIndex[textNodesIndex];
|
|
897
915
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
for (i = 0; i < childNodes.length; i++) {
|
|
904
|
-
currentNode = childNodes[i];
|
|
905
|
-
|
|
906
|
-
if (isBlacklisted(currentNode)) {
|
|
907
|
-
continue;
|
|
908
|
-
} else if (isWrappable(currentNode)) {
|
|
909
|
-
parent = currentNode.parentNode;
|
|
910
|
-
initialChildCount = parent.childNodes.length;
|
|
911
|
-
nodeInfos = highlightIndex[textNodesIndex];
|
|
912
|
-
|
|
913
|
-
if (nodeInfos.highlighted === true) {
|
|
914
|
-
if (_.isArray(nodeInfos.inlineRanges)) {
|
|
915
|
-
nodeInfos.inlineRanges.reverse();
|
|
916
|
-
nodeInfos.inlineRanges.forEach(function (inlineRange) {
|
|
917
|
-
range = document.createRange();
|
|
918
|
-
range.setStart(currentNode, inlineRange.startOffset || 0);
|
|
919
|
-
range.setEnd(currentNode, inlineRange.endOffset || currentNode.textContent.length);
|
|
920
|
-
range.surroundContents(getWrapper(inlineRange.groupId, getClassNameByColor(inlineRange.c)));
|
|
921
|
-
}); // fully highlighted text node
|
|
922
|
-
} else {
|
|
916
|
+
if (nodeInfos.highlighted === true) {
|
|
917
|
+
if (_.isArray(nodeInfos.inlineRanges)) {
|
|
918
|
+
nodeInfos.inlineRanges.reverse();
|
|
919
|
+
nodeInfos.inlineRanges.forEach(function (inlineRange) {
|
|
923
920
|
range = document.createRange();
|
|
924
|
-
range.
|
|
925
|
-
range.
|
|
926
|
-
|
|
927
|
-
|
|
921
|
+
range.setStart(currentNode, inlineRange.startOffset || 0);
|
|
922
|
+
range.setEnd(currentNode, inlineRange.endOffset || currentNode.textContent.length);
|
|
923
|
+
range.surroundContents(getWrapper(inlineRange.groupId, getClassNameByColor(inlineRange.c)));
|
|
924
|
+
}); // fully highlighted text node
|
|
925
|
+
} else {
|
|
926
|
+
range = document.createRange();
|
|
927
|
+
range.selectNodeContents(currentNode);
|
|
928
|
+
range.surroundContents(getWrapper(nodeInfos.groupId, getClassNameByColor(nodeInfos.c)));
|
|
929
|
+
} // we do want to loop over the nodes created by the wrapping operation
|
|
928
930
|
|
|
929
|
-
nodesToSkip = parent.childNodes.length - initialChildCount;
|
|
930
|
-
i += nodesToSkip;
|
|
931
|
-
}
|
|
932
931
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
restoreHighlight(currentNode, highlightIndex);
|
|
932
|
+
nodesToSkip = parent.childNodes.length - initialChildCount;
|
|
933
|
+
i += nodesToSkip;
|
|
936
934
|
}
|
|
935
|
+
|
|
936
|
+
textNodesIndex++;
|
|
937
|
+
} else if (isElement(currentNode)) {
|
|
938
|
+
restoreHighlight(currentNode, highlightIndex);
|
|
937
939
|
}
|
|
938
940
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* For `keepEmptyNodes` option, wraps the text nodes according to highlights data model.
|
|
944
|
+
* Traverses and updates DOM tree. Shouldn't throw errors in case of mismatches.
|
|
945
|
+
* @param {Node} rootNode
|
|
946
|
+
* @param {HighlightEntryKeepEmpty[]|null} highlightModel
|
|
947
|
+
*/
|
|
946
948
|
|
|
947
|
-
function restoreHighlightKeepEmpty(rootNode, highlightModel) {
|
|
948
|
-
if (!highlightModel) {
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
949
|
|
|
952
|
-
|
|
953
|
-
|
|
950
|
+
function restoreHighlightKeepEmpty(rootNode, highlightModel) {
|
|
951
|
+
if (!highlightModel) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
954
|
|
|
955
|
-
|
|
956
|
-
|
|
955
|
+
var currentModel;
|
|
956
|
+
var range;
|
|
957
957
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
let currentParentNode = rootNode;
|
|
961
|
-
let pathNotFound = false;
|
|
958
|
+
for (var k = 0; k < highlightModel.length; k++) {
|
|
959
|
+
currentModel = highlightModel[k]; //find node to wrap - go through nodes until we reach level where node to wrap will be
|
|
962
960
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
961
|
+
var childNodes = void 0;
|
|
962
|
+
var indexInModel = void 0;
|
|
963
|
+
var currentParentNode = rootNode;
|
|
964
|
+
var pathNotFound = false;
|
|
966
965
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
indexInModel = currentModel.path[m];
|
|
971
|
-
currentParentNode = childNodes[indexInModel];
|
|
966
|
+
if (!currentModel.path || !currentModel.path.length) {
|
|
967
|
+
continue; //something went wrong
|
|
968
|
+
}
|
|
972
969
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
970
|
+
for (var m = 0; m < currentModel.path.length; m++) {
|
|
971
|
+
//path was counted among non-empty nodes
|
|
972
|
+
childNodes = Array.from(currentParentNode.childNodes).filter(function (node) {
|
|
973
|
+
return !(isText(node) && !node.textContent.length);
|
|
974
|
+
});
|
|
975
|
+
indexInModel = currentModel.path[m];
|
|
976
|
+
currentParentNode = childNodes[indexInModel];
|
|
977
|
+
|
|
978
|
+
if (!currentParentNode && m < currentModel.path.length - 1) {
|
|
979
|
+
//node on last level may not exist yet, no need to fail. See `nodeAtIndex`
|
|
980
|
+
pathNotFound = true;
|
|
981
|
+
break;
|
|
978
982
|
}
|
|
983
|
+
}
|
|
979
984
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
985
|
+
if (pathNotFound) {
|
|
986
|
+
continue; //something went wrong
|
|
987
|
+
} //add single highlight
|
|
983
988
|
|
|
984
989
|
|
|
985
|
-
|
|
990
|
+
var nodeAtIndex = null;
|
|
986
991
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
992
|
+
if (!currentModel.offsetBefore) {
|
|
993
|
+
//wrap starts on this node
|
|
994
|
+
nodeAtIndex = childNodes[indexInModel];
|
|
990
995
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
996
|
+
if (!nodeAtIndex || !isText(nodeAtIndex) || isBlacklisted(nodeAtIndex)) {
|
|
997
|
+
continue; //something went wrong
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
//split previousSibling to create a node for wrapping
|
|
1001
|
+
var nodeBefore = childNodes[indexInModel - 1];
|
|
997
1002
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1003
|
+
if (!nodeBefore || !isText(nodeBefore) || nodeBefore.textContent.length <= currentModel.offsetBefore || isBlacklisted(nodeBefore)) {
|
|
1004
|
+
continue; //something went wrong
|
|
1005
|
+
}
|
|
1001
1006
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1007
|
+
nodeAtIndex = nodeBefore.splitText(currentModel.offsetBefore);
|
|
1008
|
+
} //cut off its end
|
|
1004
1009
|
|
|
1005
1010
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1011
|
+
if (nodeAtIndex.textContent.length > currentModel.textLength) {
|
|
1012
|
+
nodeAtIndex.splitText(currentModel.textLength);
|
|
1013
|
+
} //wrap
|
|
1009
1014
|
|
|
1010
1015
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1016
|
+
var wrapperNode = getWrapper(currentModel.groupId, getClassNameByColor(currentModel.c));
|
|
1017
|
+
addSplitData(wrapperNode, currentModel.beforeWasSplit, currentModel.afterWasSplit);
|
|
1018
|
+
range = document.createRange();
|
|
1019
|
+
range.selectNodeContents(nodeAtIndex);
|
|
1020
|
+
rangeSurroundContentsNoEmptyNodes(range, wrapperNode);
|
|
1017
1021
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Set highlighter color
|
|
1025
|
+
* @param {string} color Active highlighter color
|
|
1026
|
+
*/
|
|
1022
1027
|
|
|
1023
1028
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1029
|
+
var setActiveColor = function setActiveColor(color) {
|
|
1030
|
+
if (options.colors[color]) {
|
|
1031
|
+
className = options.colors[color];
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
/**
|
|
1035
|
+
* Helpers
|
|
1036
|
+
*/
|
|
1032
1037
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1038
|
+
/**
|
|
1039
|
+
* Return the object key contains the given value
|
|
1040
|
+
* @param {Object} object
|
|
1041
|
+
* @param {any} value
|
|
1042
|
+
* @return {string|undefined}
|
|
1043
|
+
*/
|
|
1039
1044
|
|
|
1040
1045
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1046
|
+
var getKeyByValue = function getKeyByValue(object, value) {
|
|
1047
|
+
return Object.keys(object).find(function (key) {
|
|
1048
|
+
return object[key] === value;
|
|
1049
|
+
});
|
|
1050
|
+
};
|
|
1051
|
+
/**
|
|
1052
|
+
* Returns color identifier for the given class name
|
|
1053
|
+
* @param {string} highlighterClassName Class name of highlighter classes
|
|
1054
|
+
* @returns {string|number} Color identifier
|
|
1055
|
+
*/
|
|
1047
1056
|
|
|
1048
1057
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1058
|
+
var getColorByClassName = function getColorByClassName(highlighterClassName) {
|
|
1059
|
+
if (options.colors) {
|
|
1060
|
+
return getKeyByValue(options.colors, highlighterClassName);
|
|
1061
|
+
}
|
|
1053
1062
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1063
|
+
return className;
|
|
1064
|
+
};
|
|
1065
|
+
/**
|
|
1066
|
+
* Returns class name for the given color identifier
|
|
1067
|
+
* @param {string|number} color Color identifier
|
|
1068
|
+
* @returns {string} Class name
|
|
1069
|
+
*/
|
|
1061
1070
|
|
|
1062
1071
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1072
|
+
var getClassNameByColor = function getClassNameByColor(color) {
|
|
1073
|
+
if (options.colors && options.colors[color]) {
|
|
1074
|
+
return options.colors[color];
|
|
1075
|
+
}
|
|
1067
1076
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1077
|
+
return className;
|
|
1078
|
+
};
|
|
1079
|
+
/**
|
|
1080
|
+
* Check if the given node is a wrapper
|
|
1081
|
+
* @param {Node|Element} node
|
|
1082
|
+
* @returns {boolean}
|
|
1083
|
+
*/
|
|
1075
1084
|
|
|
1076
1085
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1086
|
+
function isWrappingNode(node) {
|
|
1087
|
+
return isElement(node) && node.tagName.toLowerCase() === 'span' && highlightingClasses.includes(node.className);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if the given node can be wrapped
|
|
1091
|
+
* @param {Node} node
|
|
1092
|
+
* @returns {boolean}
|
|
1093
|
+
*/
|
|
1085
1094
|
|
|
1086
1095
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1096
|
+
function isWrappable(node) {
|
|
1097
|
+
return isText(node) && !isBlacklisted(node);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Check if the given node is, or is within, a blacklisted container.
|
|
1101
|
+
* With `keepEmptyNodes` option, node inside blacklisted container can be whitelisted too.
|
|
1102
|
+
* Priority of blacklist or whitelist is decided by which selector is closest to the node.
|
|
1103
|
+
* If no match found, node is considered whitelisted.
|
|
1104
|
+
* @param {Node} node
|
|
1105
|
+
* @returns {boolean}
|
|
1106
|
+
*/
|
|
1098
1107
|
|
|
1099
1108
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1109
|
+
function isBlacklisted(node) {
|
|
1110
|
+
var closest = $(node).closest(containersBlackAndWhiteListSelector);
|
|
1102
1111
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1112
|
+
if (!closest.length) {
|
|
1113
|
+
return false;
|
|
1114
|
+
} else if (!containersWhiteListSelector) {
|
|
1115
|
+
return true;
|
|
1116
|
+
} else {
|
|
1117
|
+
return !closest.get(0).matches(containersWhiteListSelector);
|
|
1110
1118
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Wraps text node to the highlighter wrapper element
|
|
1122
|
+
* @param {Node} textNode Text node to wrap
|
|
1123
|
+
* @param {string} className Wrapper class name
|
|
1124
|
+
* @param {number} groupId Group id
|
|
1125
|
+
*/
|
|
1117
1126
|
|
|
1118
1127
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1128
|
+
function wrapNode(textNode, className, groupId) {
|
|
1129
|
+
var element = getWrapper(groupId, className);
|
|
1130
|
+
element.appendChild(textNode);
|
|
1131
|
+
return element;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Create a wrapping node
|
|
1135
|
+
* @param {number} groupId
|
|
1136
|
+
* @returns {Element}
|
|
1137
|
+
*/
|
|
1129
1138
|
|
|
1130
1139
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1140
|
+
function getWrapper(groupId, wrapperClass) {
|
|
1141
|
+
var wrapper = document.createElement('span');
|
|
1142
|
+
wrapper.className = wrapperClass || className;
|
|
1143
|
+
wrapper.setAttribute(GROUP_ATTR, "".concat(groupId));
|
|
1144
|
+
return wrapper;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Returns the first unused group Id
|
|
1148
|
+
* @returns {number}
|
|
1149
|
+
*/
|
|
1141
1150
|
|
|
1142
1151
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1152
|
+
function getAvailableGroupId() {
|
|
1153
|
+
var id = currentGroupId || 1;
|
|
1145
1154
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
return id;
|
|
1155
|
+
while ($(getContainer()).find('[' + GROUP_ATTR + '=' + id + ']').length !== 0) {
|
|
1156
|
+
id++;
|
|
1151
1157
|
}
|
|
1152
|
-
/**
|
|
1153
|
-
* Check if the given node is an element
|
|
1154
|
-
* @param {Node} node
|
|
1155
|
-
* @returns {boolean}
|
|
1156
|
-
*/
|
|
1157
|
-
|
|
1158
1158
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
*/
|
|
1159
|
+
return id;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Check if the given node is an element
|
|
1163
|
+
* @param {Node} node
|
|
1164
|
+
* @returns {boolean}
|
|
1165
|
+
*/
|
|
1167
1166
|
|
|
1168
1167
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1168
|
+
function isElement(node) {
|
|
1169
|
+
return node && _typeof(node) === 'object' && node.nodeType === window.Node.ELEMENT_NODE;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Check if the given node is of type text
|
|
1173
|
+
* @param {Node} node
|
|
1174
|
+
* @returns {boolean}
|
|
1175
|
+
*/
|
|
1177
1176
|
|
|
1178
1177
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1178
|
+
function isText(node) {
|
|
1179
|
+
return node && _typeof(node) === 'object' && node.nodeType === window.Node.TEXT_NODE;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* a "Hot Node" is either wrappable text node or a wrapper
|
|
1183
|
+
* @param {Node} node
|
|
1184
|
+
* @returns {boolean}
|
|
1185
|
+
*/
|
|
1185
1186
|
|
|
1186
1187
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
highlightFromIndex: highlightFromIndex,
|
|
1190
|
-
getHighlightIndex: getHighlightIndex,
|
|
1191
|
-
clearHighlights: clearHighlights,
|
|
1192
|
-
clearSingleHighlight: clearSingleHighlight,
|
|
1193
|
-
setActiveColor
|
|
1194
|
-
};
|
|
1188
|
+
function isHotNode(node) {
|
|
1189
|
+
return isWrappingNode(node) || isWrappable(node);
|
|
1195
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Public API of the highlighter helper
|
|
1193
|
+
*/
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
highlightRanges: highlightRanges,
|
|
1198
|
+
highlightFromIndex: highlightFromIndex,
|
|
1199
|
+
getHighlightIndex: getHighlightIndex,
|
|
1200
|
+
clearHighlights: clearHighlights,
|
|
1201
|
+
clearSingleHighlight: clearSingleHighlight,
|
|
1202
|
+
setActiveColor: setActiveColor
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1196
1205
|
|
|
1197
|
-
|
|
1206
|
+
return highlighter;
|
|
1198
1207
|
|
|
1199
1208
|
});
|