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