diff --git a/src/components/SettingsSection/SettingsSection.vue b/src/components/SettingsSection/SettingsSection.vue
index bc66086b14..764a42434a 100644
--- a/src/components/SettingsSection/SettingsSection.vue
+++ b/src/components/SettingsSection/SettingsSection.vue
@@ -49,7 +49,7 @@ This component is to be used in the settings section of nextcloud.
class="settings-section__info"
role="note"
:title="docTitleTranslated">
-
+
import Popover from '../Popover'
+import UserBubbleDiv from './UserBubbleDiv'
import Avatar from '../Avatar'
export default {
@@ -119,6 +120,7 @@ export default {
components: {
Popover,
Avatar,
+ UserBubbleDiv,
},
props: {
/**
@@ -198,12 +200,14 @@ export default {
/**
* If userbubble is empty, let's NOT
* use the Popover component
- * @returns {string} 'Popover' or 'div'
+ * We need a component instead of a simple div here,
+ * because otherwise the trigger template will not be shown.
+ * @returns {string} 'Popover' or 'UserBubbleDiv'
*/
isPopoverComponent() {
return !this.popoverEmpty
? 'Popover'
- : 'div'
+ : 'UserBubbleDiv'
},
/**
diff --git a/src/components/UserBubble/UserBubbleDiv.vue b/src/components/UserBubble/UserBubbleDiv.vue
new file mode 100644
index 0000000000..7f0e2a7688
--- /dev/null
+++ b/src/components/UserBubble/UserBubbleDiv.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/index.js b/src/components/index.js
index b1555216d2..4193715761 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -21,98 +21,50 @@
*
*/
-import ActionButton from './ActionButton'
-import ActionCaption from './ActionCaption'
-import ActionCheckbox from './ActionCheckbox'
-import ActionInput from './ActionInput'
-import ActionLink from './ActionLink'
-import ActionRadio from './ActionRadio'
-import ActionRouter from './ActionRouter'
-import Actions from './Actions'
-import ActionSeparator from './ActionSeparator'
-import ActionText from './ActionText'
-import ActionTextEditable from './ActionTextEditable'
-import AppContent from './AppContent'
-import AppContentDetails from './AppContentDetails'
-import AppContentList from './AppContentList'
-import AppNavigation from './AppNavigation'
-import AppNavigationCaption from './AppNavigationCaption'
-import AppNavigationCounter from './AppNavigationCounter'
-import AppNavigationIconBullet from './AppNavigationIconBullet'
-import AppNavigationItem from './AppNavigationItem'
-import AppNavigationNew from './AppNavigationNew'
-import AppNavigationNewItem from './AppNavigationNewItem'
-import AppNavigationSettings from './AppNavigationSettings'
-import AppNavigationSpacer from './AppNavigationSpacer'
-import AppSettingsDialog from './AppSettingsDialog'
-import AppSettingsSection from './AppSettingsSection'
-import AppSidebar from './AppSidebar'
-import AppSidebarTab from './AppSidebarTab'
-import Avatar from './Avatar'
-import Breadcrumb from './Breadcrumb'
-import Breadcrumbs from './Breadcrumbs'
-import CheckboxRadioSwitch from './CheckboxRadioSwitch'
-import ColorPicker from './ColorPicker'
-import Content from './Content'
-import CounterBubble from './CounterBubble'
-import DatetimePicker from './DatetimePicker'
-import EmptyContent from './EmptyContent'
-import ListItem from './ListItem'
-import ListItemIcon from './ListItemIcon'
-import Modal from './Modal'
-import Multiselect from './Multiselect'
-import MultiselectTags from './MultiselectTags'
-import Popover from './Popover'
-import PopoverMenu from './PopoverMenu'
-import RichContenteditable from './RichContenteditable'
-import SettingsSection from './SettingsSection'
-import UserBubble from './UserBubble'
-
-export {
- ActionButton,
- ActionCheckbox,
- ActionInput,
- ActionLink,
- ActionRadio,
- ActionRouter,
- Actions,
- ActionSeparator,
- ActionText,
- ActionTextEditable,
- AppContent,
- AppContentDetails,
- AppContentList,
- AppNavigation,
- AppNavigationCaption,
- AppNavigationCounter,
- AppNavigationIconBullet,
- AppNavigationItem,
- AppNavigationNew,
- AppNavigationNewItem,
- AppNavigationSettings,
- AppNavigationSpacer,
- AppSettingsDialog,
- AppSettingsSection,
- AppSidebar,
- AppSidebarTab,
- Avatar,
- Breadcrumb,
- Breadcrumbs,
- CheckboxRadioSwitch,
- ColorPicker,
- Content,
- CounterBubble,
- DatetimePicker,
- EmptyContent,
- ListItem,
- ListItemIcon,
- Modal,
- Multiselect,
- MultiselectTags,
- Popover,
- PopoverMenu,
- RichContenteditable,
- SettingsSection,
- UserBubble,
- ActionCaption,
-}
+export { default as ActionButton } from './ActionButton'
+export { default as ActionCaption } from './ActionCaption'
+export { default as ActionCheckbox } from './ActionCheckbox'
+export { default as ActionInput } from './ActionInput'
+export { default as ActionLink } from './ActionLink'
+export { default as ActionRadio } from './ActionRadio'
+export { default as ActionRouter } from './ActionRouter'
+export { default as Actions } from './Actions'
+export { default as ActionSeparator } from './ActionSeparator'
+export { default as ActionText } from './ActionText'
+export { default as ActionTextEditable } from './ActionTextEditable'
+export { default as AppContent } from './AppContent'
+export { default as AppContentDetails } from './AppContentDetails'
+export { default as AppContentList } from './AppContentList'
+export { default as AppNavigation } from './AppNavigation'
+export { default as AppNavigationCaption } from './AppNavigationCaption'
+export { default as AppNavigationCounter } from './AppNavigationCounter'
+export { default as AppNavigationIconBullet } from './AppNavigationIconBullet'
+export { default as AppNavigationItem } from './AppNavigationItem'
+export { default as AppNavigationNew } from './AppNavigationNew'
+export { default as AppNavigationNewItem } from './AppNavigationNewItem'
+export { default as AppNavigationSettings } from './AppNavigationSettings'
+export { default as AppNavigationSpacer } from './AppNavigationSpacer'
+export { default as AppSettingsDialog } from './AppSettingsDialog'
+export { default as AppSettingsSection } from './AppSettingsSection'
+export { default as AppSidebar } from './AppSidebar'
+export { default as AppSidebarTab } from './AppSidebarTab'
+export { default as Avatar } from './Avatar'
+export { default as Breadcrumb } from './Breadcrumb'
+export { default as Breadcrumbs } from './Breadcrumbs'
+export { default as CheckboxRadioSwitch } from './CheckboxRadioSwitch'
+export { default as ColorPicker } from './ColorPicker'
+export { default as Content } from './Content'
+export { default as CounterBubble } from './CounterBubble'
+export { default as DatetimePicker } from './DatetimePicker'
+export { default as EmptyContent } from './EmptyContent'
+export { default as ListItem } from './ListItem'
+export { default as ListItemIcon } from './ListItemIcon'
+export { default as Modal } from './Modal'
+export { default as Multiselect } from './Multiselect'
+export { default as MultiselectTags } from './MultiselectTags'
+export { default as Popover } from './Popover'
+export { default as PopoverMenu } from './PopoverMenu'
+export { default as RichContenteditable } from './RichContenteditable'
+export { default as SettingsSection } from './SettingsSection'
+export { default as UserBubble } from './UserBubble'
+export { default as Button } from './Button'
diff --git a/src/directives/Linkify/index.js b/src/directives/Linkify/index.js
index 3c74cb6ba8..f9d7f08a5c 100644
--- a/src/directives/Linkify/index.js
+++ b/src/directives/Linkify/index.js
@@ -16,15 +16,13 @@
* License along with this library. If not, see .
*
*/
-import linkifyStr from 'linkifyjs/string'
+import Linkify from '../../utils/Linkify'
// Use function shorthand for same behavior on bind and update
// https://vuejs.org/v2/guide/custom-directive.html#Function-Shorthand
export const directive = function(el, binding) {
if (binding.value?.linkify === true) {
- el.innerHTML = linkifyStr(binding.value.text, {
- defaultProtocol: 'https',
- })
+ el.innerHTML = Linkify(binding.value.text)
}
}
diff --git a/src/directives/index.js b/src/directives/index.js
index 55ed52ff4f..5cbe83d49a 100644
--- a/src/directives/index.js
+++ b/src/directives/index.js
@@ -20,12 +20,6 @@
*
*/
-import Focus from './Focus'
-import Linkify from './Linkify'
-import Tooltip from './Tooltip'
-
-export {
- Focus,
- Linkify,
- Tooltip,
-}
+export { default as Focus } from './Focus'
+export { default as Linkify } from './Linkify'
+export { default as Tooltip } from './Tooltip'
diff --git a/src/functions/emoji/emoji.js b/src/functions/emoji/emoji.js
new file mode 100644
index 0000000000..126d327d66
--- /dev/null
+++ b/src/functions/emoji/emoji.js
@@ -0,0 +1,46 @@
+/**
+ * @copyright Copyright (c) 2021 Jonas Meurer
+ *
+ * @author Jonas Meurer
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+import data from 'emoji-mart-vue-fast/data/all.json'
+import { EmojiIndex, frequently } from 'emoji-mart-vue-fast'
+
+// export const allEmojis = index.buildIndex()
+
+/**
+ * @param {string} query Emoji search string
+ * @param {Number} maxResults Maximum of returned emojis
+ * @returns {Array} list of found emojis
+ */
+export const emojiSearch = function(query, maxResults = 10) {
+ const index = new EmojiIndex(data)
+ if (query) {
+ return index.search(query, maxResults) || []
+ }
+
+ return frequently.get(maxResults).map((id) => index.emoji(id)) || []
+}
+
+export const addRecent = function(id) {
+ frequently.add(id)
+}
+
+export default { emojiSearch, addRecent }
diff --git a/src/functions/emoji/index.js b/src/functions/emoji/index.js
new file mode 100644
index 0000000000..361216aa7b
--- /dev/null
+++ b/src/functions/emoji/index.js
@@ -0,0 +1,25 @@
+/**
+ * @copyright Copyright (c) 2021 Jonas Meurer
+ *
+ * @author Jonas Meurer
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+import { emojiSearch, addRecent } from './emoji'
+
+export { emojiSearch, addRecent }
diff --git a/src/mixins/index.js b/src/mixins/index.js
index 3d68c2a391..e330938705 100644
--- a/src/mixins/index.js
+++ b/src/mixins/index.js
@@ -20,16 +20,8 @@
*
*/
-import excludeClickOutsideClasses from './excludeClickOutsideClasses'
-import isFullscreen from './isFullscreen'
-import isMobile from './isMobile'
-import richEditor from './richEditor'
-import userStatus from './userStatus'
-
-export {
- excludeClickOutsideClasses,
- isFullscreen,
- isMobile,
- richEditor,
- userStatus,
-}
+export { default as excludeClickOutsideClasses } from './excludeClickOutsideClasses'
+export { default as isFullscreen } from './isFullscreen'
+export { default as isMobile } from './isMobile'
+export { default as richEditor } from './richEditor'
+export { default as userStatus } from './userStatus'
diff --git a/src/mixins/richEditor/index.js b/src/mixins/richEditor/index.js
index bceaca4544..1f03825b81 100644
--- a/src/mixins/richEditor/index.js
+++ b/src/mixins/richEditor/index.js
@@ -21,7 +21,7 @@
*/
import escapeHtml from 'escape-html'
-import linkifyStr from 'linkifyjs/string'
+import Linkify from '../../utils/Linkify'
import stripTags from 'striptags'
import Vue from 'vue'
@@ -63,14 +63,7 @@ export default {
// on the the uneven indexes. We only want to generate the mentions html
if (!part.startsWith('@')) {
// This part doesn't contain a mention, let's make sure links are parsed
- return linkifyStr(part, {
- defaultProtocol: 'https',
- target: '_blank',
- className: 'external',
- attributes: {
- rel: 'noopener noreferrer',
- },
- })
+ return Linkify(part)
}
// Extracting the id, nuking the " and @
diff --git a/src/utils/FindRanges.js b/src/utils/FindRanges.js
index d4c25987c2..90ccbbd5ea 100644
--- a/src/utils/FindRanges.js
+++ b/src/utils/FindRanges.js
@@ -38,7 +38,7 @@ const FindRanges = (text, search) => {
currentIndex = index + search.length
ranges.push({ start: index, end: currentIndex })
- index = text.toLowerCase().indexOf(search.toLowerCase(), index + 1)
+ index = text.toLowerCase().indexOf(search.toLowerCase(), currentIndex)
i++
}
return ranges
diff --git a/src/utils/Linkify.js b/src/utils/Linkify.js
new file mode 100644
index 0000000000..6d72c8162a
--- /dev/null
+++ b/src/utils/Linkify.js
@@ -0,0 +1,41 @@
+/**
+ * @copyright Copyright (c) 2021 Raimund Schlüßler
+ *
+ * @author Raimund Schlüßler
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+import linkifyStr from 'linkify-string'
+
+/**
+ * Linkify text
+ *
+ * @param {string} text The text to linkify
+ * @returns {string} The linkified string
+ */
+const Linkify = (text) => {
+ return linkifyStr(text, {
+ defaultProtocol: 'https',
+ target: '_blank',
+ className: 'external linkified',
+ attributes: {
+ rel: 'nofollow noopener noreferrer',
+ },
+ })
+}
+
+export default Linkify
diff --git a/styleguide.config.js b/styleguide.config.js
index 80ba3e816e..1aa48c6b75 100644
--- a/styleguide.config.js
+++ b/styleguide.config.js
@@ -76,6 +76,7 @@ module.exports = {
'src/components/*Picker/*.vue',
'src/components/RichContenteditable/!(RichContenteditable).vue',
'src/components/Settings*/*.vue',
+ 'src/components/UserBubble/UserBubbleDiv.vue',
],
sections: [
{
diff --git a/styleguide/assets/server.css b/styleguide/assets/server.css
index 8257a9ce9b..1cfa800694 100644
--- a/styleguide/assets/server.css
+++ b/styleguide/assets/server.css
@@ -1373,7 +1373,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
}
/* Simple selector to allow easy overriding */
/* line 26, /var/www/nextcloud/core/css/inputs.scss */
- select, button, input, textarea, div[contenteditable=true], div[contenteditable=false] {
+ select, button:not(.button-vue), input, textarea, div[contenteditable=true], div[contenteditable=false] {
width: 130px;
min-height: 34px;
box-sizing: border-box;
@@ -1385,7 +1385,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
*/
/* Default global values */
/* line 44, /var/www/nextcloud/core/css/inputs.scss */
- div.select2-drop .select2-search input, select, button, .button, input:not([type='range']), textarea, div[contenteditable=true], .pager li a {
+ div.select2-drop .select2-search input, select, button:not(.button-vue), .button, input:not([type='range']), textarea, div[contenteditable=true], .pager li a {
margin: 3px 3px 3px 0;
padding: 7px 6px;
font-size: 13px;
@@ -1404,7 +1404,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
outline: none;
}
/* line 68, /var/www/nextcloud/core/css/inputs.scss */
- div.select2-drop .select2-search input:not(:disabled):not(.primary):active, select:not(:disabled):not(.primary):active, button:not(:disabled):not(.primary):active, .button:not(:disabled):not(.primary):active, input:not([type='range']):not(:disabled):not(.primary):active, textarea:not(:disabled):not(.primary):active, div[contenteditable=true]:not(:disabled):not(.primary):active, .pager li a:not(:disabled):not(.primary):active {
+ div.select2-drop .select2-search input:not(:disabled):not(.primary):active, select:not(:disabled):not(.primary):active, button:not(:disabled):not(.primary):not(.button-vue):active, input:not([type='range']):not(:disabled):not(.primary):active, textarea:not(:disabled):not(.primary):active, div[contenteditable=true]:not(:disabled):not(.primary):active, .pager li a:not(:disabled):not(.primary):active {
outline: none;
background-color: var(--color-main-background);
color: var(--color-text-light);
@@ -1503,7 +1503,7 @@ html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pr
}
/* 'Click' inputs */
/* line 170, /var/www/nextcloud/core/css/inputs.scss */
- select, button, .button, input[type='button'], input[type='submit'], input[type='reset'] {
+ select, button:not(.button-vue), .button, input[type='button'], input[type='submit'], input[type='reset'] {
padding: 6px 12px;
width: auto;
min-height: 34px;
diff --git a/styleguide/assets/variables.css b/styleguide/assets/variables.css
index 4968f46fab..dcf88ace5b 100644
--- a/styleguide/assets/variables.css
+++ b/styleguide/assets/variables.css
@@ -1,29 +1,37 @@
:root {
--color-main-text: #222;
--color-main-background: #fff;
- --color-main-background-translucent: rgba(255, 255, 255, 1);
- --color-background-hover: #f5f5f5;
+ --color-main-background-translucent: rgba(255, 255, 255, 0.97);
+ --gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
+ --color-background-hover: whitesmoke;
--color-background-dark: #ededed;
--color-background-darker: #dbdbdb;
--color-placeholder-light: #e6e6e6;
--color-placeholder-dark: #ccc;
--color-primary: #0082c9;
- --color-primary-light: #fbf0f4;
- --color-primary-text: #fff;
+ --color-primary-hover: #339bd4;
+ --color-primary-light: #e6f3fa;
+ --color-primary-light-hover: #dce8ef;
+ --color-primary-text: #ffffff;
+ --color-primary-light-text: #0082c9;
--color-primary-text-dark: #ededed;
--color-primary-element: #0082c9;
+ --color-primary-element-hover: #339bd4;
--color-primary-element-light: #17adff;
- --color-primary-light: #e6f3fa;
+ --color-primary-element-lighter: #d9ecf7;
--color-error: #e9322d;
+ --color-error-hover: #ed5b57;
--color-warning: #eca700;
+ --color-warning-hover: #f0b933;
--color-success: #46ba61;
+ --color-success-hover: #6bc881;
--color-text-maxcontrast: #767676;
--color-text-light: #222;
--color-text-lighter: #767676;
- /* --image-logo: url('/core/img/logo/logo.png?v=12');
- --image-login-background: url('/core/img/background.png?v=12');
- --image-logoheader: url('/core/img/logo/logo.png?v=12');
- --image-favicon: url('/core/img/logo/logo.png?v=12'); */
+ /* --image-logo: url(/core/img/logo/logo.png?v=0);
+ --image-login-background: url(/core/img/background.png?v=0);
+ --image-logoheader: url(/core/img/logo/logo.png?v=0);
+ --image-favicon: url(/core/img/logo/logo.png?v=0); */
--color-loading-light: #ccc;
--color-loading-dark: #444;
--color-box-shadow: rgba(77, 77, 77, 0.5);
diff --git a/styleguide/global.requires.js b/styleguide/global.requires.js
index 596b4ec85c..ee27487374 100644
--- a/styleguide/global.requires.js
+++ b/styleguide/global.requires.js
@@ -86,9 +86,6 @@ window.OC = {
window.OCA = {}
window.appName = 'nextcloud-vue'
-window.t = (app, text) => text
-
-Vue.prototype.t = window.t
Vue.prototype.OC = window.OC
Vue.prototype.OCA = window.OCA
diff --git a/tests/unit/components/Highlight/Highlight.spec.js b/tests/unit/components/Highlight/Highlight.spec.js
new file mode 100644
index 0000000000..31382e59fd
--- /dev/null
+++ b/tests/unit/components/Highlight/Highlight.spec.js
@@ -0,0 +1,140 @@
+/**
+ * @copyright Copyright (c) 2021 Raimund Schlüßler
+ *
+ * @author Raimund Schlüßler
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+import { mount } from '@vue/test-utils'
+import Highlight from '../../../../src/components/Highlight/Highlight.vue'
+
+describe('Highlight.vue', () => {
+ 'use strict'
+
+ describe('validate given ranges', () => {
+ it('should ensure ranges are well formed (start before end)', () => {
+ const wrapper = mount(Highlight, {
+ propsData: {
+ text: 'Highlight me',
+ search: 'me',
+ highlight: [
+ { start: 3, end: 1},
+ { start: 5, end: 7},
+ ]
+ },
+ })
+
+ expect(wrapper.vm.ranges).toEqual([
+ {start: 1, end: 3},
+ {start: 5, end: 7},
+ ])
+ })
+
+ it('should discard ranges completely out of bound', () => {
+ const wrapper = mount(Highlight, {
+ propsData: {
+ text: 'Highlight me',
+ search: 'me',
+ highlight: [
+ { start: -10, end: -2 },
+ { start: 1, end: 3 },
+ { start: 5, end: 7 },
+ { start: 20, end: 25 },
+ ]
+ },
+ })
+
+ expect(wrapper.vm.ranges).toEqual([
+ {start: 1, end: 3},
+ {start: 5, end: 7},
+ ])
+ })
+
+ it('should limit ranges to the string length', () => {
+ const wrapper = mount(Highlight, {
+ propsData: {
+ text: 'Highlight me',
+ search: 'me',
+ highlight: [
+ { start: -10, end: -2 },
+ { start: -2, end: 1 },
+ { start: 3, end: 3 },
+ { start: 5, end: 7 },
+ { start: 10, end: 25 },
+ { start: 20, end: 25 },
+ ]
+ },
+ })
+
+ expect(wrapper.vm.ranges).toEqual([
+ {start: 0, end: 1},
+ {start: 3, end: 3},
+ {start: 5, end: 7},
+ {start: 10, end: 12},
+ ])
+ })
+
+ it('should sort ranges ascendingly', () => {
+ const wrapper = mount(Highlight, {
+ propsData: {
+ text: 'Highlight me',
+ search: 'me',
+ highlight: [
+ { start: -10, end: -2 },
+ { start: -2, end: 1 },
+ { start: 20, end: 25 },
+ { start: 10, end: 25 },
+ { start: 5, end: 7 },
+ { start: 3, end: 3 },
+ ]
+ },
+ })
+
+ expect(wrapper.vm.ranges).toEqual([
+ {start: 0, end: 1},
+ {start: 3, end: 3},
+ {start: 5, end: 7},
+ {start: 10, end: 12},
+ ])
+ })
+
+ it('should merge overlapping or adjacent ranges', () => {
+ const wrapper = mount(Highlight, {
+ propsData: {
+ text: 'Highlight me',
+ search: 'me',
+ highlight: [
+ { start: -2, end: 1 },
+ { start: 1, end: 3 },
+ { start: 5, end: 7 },
+ { start: 6, end: 25 },
+ { start: 6, end: 25 },
+ { start: 7, end: 9 },
+ { start: 20, end: 25 },
+ { start: -10, end: -2 },
+ ]
+ },
+ })
+
+ expect(wrapper.vm.ranges).toEqual([
+ {start: 0, end: 3},
+ {start: 5, end: 12},
+ ])
+ })
+ })
+})
diff --git a/tests/unit/utils/FindRanges.spec.js b/tests/unit/utils/FindRanges.spec.js
new file mode 100644
index 0000000000..4ce0719f3a
--- /dev/null
+++ b/tests/unit/utils/FindRanges.spec.js
@@ -0,0 +1,69 @@
+/**
+ * @copyright Copyright (c) 2021 Raimund Schlüßler
+ *
+ * @author Raimund Schlüßler
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+import FindRanges from '../../../src/utils/FindRanges'
+
+describe('FindRanges.js', () => {
+ 'use strict'
+
+ describe('find matching ranges', () => {
+ it('should find the matching range', () => {
+ const ranges = FindRanges('ananas', 'anan')
+
+ expect(ranges).toEqual([
+ {start: 0, end: 4},
+ ])
+ })
+
+ it('should find all non-overlapping ranges', () => {
+ const ranges1 = FindRanges('ananas', 'an')
+
+ expect(ranges1).toEqual([
+ {start: 0, end: 2},
+ {start: 2, end: 4},
+ ])
+
+ const ranges2 = FindRanges('ananas', 'a')
+
+ expect(ranges2).toEqual([
+ {start: 0, end: 1},
+ {start: 2, end: 3},
+ {start: 4, end: 5},
+ ])
+ })
+
+ it('should only find first occurence of overlapping ranges', () => {
+ const ranges1 = FindRanges('ananas', 'ana')
+
+ expect(ranges1).toEqual([
+ {start: 0, end: 3},
+ ])
+
+ const ranges2 = FindRanges('oooo', 'oo')
+
+ expect(ranges2).toEqual([
+ {start: 0, end: 2},
+ {start: 2, end: 4},
+ ])
+ })
+ })
+})