forked from WorldBrain/Memex
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinteractions.js
242 lines (210 loc) · 7.36 KB
/
interactions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { browser } from 'webextension-polyfill-ts'
import { delayed, getPositionState, getTooltipState } from './utils'
import {
createAndCopyDirectLink,
createAnnotation,
createHighlight,
} from '../direct-linking/content_script/interactions'
import { setupUIContainer, destroyUIContainer } from './components'
import { remoteFunction, makeRemotelyCallable } from '../util/webextensionRPC'
import { injectCSS } from '../search-injection/dom'
import { conditionallyShowHighlightNotification } from './onboarding-interactions'
const openOptionsRPC = remoteFunction('openOptionsTab')
let mouseupListener = null
export function setupTooltipTrigger(callback, toolbarNotifications) {
mouseupListener = event => {
conditionallyTriggerTooltip({ callback, toolbarNotifications }, event)
}
document.body.addEventListener('mouseup', mouseupListener)
}
export function destroyTooltipTrigger() {
document.body.removeEventListener('mouseup', mouseupListener)
mouseupListener = null
}
const CLOSE_MESSAGESHOWN_KEY = 'tooltip.close-message-shown'
async function _setCloseMessageShown() {
await browser.storage.local.set({
[CLOSE_MESSAGESHOWN_KEY]: true,
})
}
async function _getCloseMessageShown() {
const {
[CLOSE_MESSAGESHOWN_KEY]: closeMessageShown,
} = await browser.storage.local.get({ [CLOSE_MESSAGESHOWN_KEY]: false })
return closeMessageShown
}
// Target container for the Tooltip.
let target = null
let showTooltip = null
/* Denotes whether the user inserted/removed tooltip by his/her own self. */
let manualOverride = false
/**
* Creates target container for Tooltip.
* Injects content_script.css.
* Mounts Tooltip React component.
* Sets up Container <---> webpage Remote functions.
*/
export const insertTooltip = async ({ toolbarNotifications }) => {
// If target is set, Tooltip has already been injected.
if (target) {
return
}
target = document.createElement('div')
target.setAttribute('id', 'memex-direct-linking-tooltip')
document.body.appendChild(target)
const cssFile = browser.extension.getURL('/content_script.css')
injectCSS(cssFile)
showTooltip = await setupUIContainer(target, {
createAndCopyDirectLink,
createAnnotation,
createHighlight,
openSettings: () => openOptionsRPC('settings'),
destroyTooltip: async () => {
manualOverride = true
removeTooltip()
const closeMessageShown = await _getCloseMessageShown()
if (!closeMessageShown) {
toolbarNotifications.showToolbarNotification(
'tooltip-first-close',
)
_setCloseMessageShown()
}
},
})
setupTooltipTrigger(showTooltip, toolbarNotifications)
conditionallyTriggerTooltip({ callback: showTooltip, toolbarNotifications })
}
export const removeTooltip = () => {
if (!target) {
return
}
destroyTooltipTrigger()
destroyUIContainer(target)
target.remove()
target = null
showTooltip = null
}
/**
* Inserts or removes tooltip from the page (if not overridden manually).
* Should either be called through the RPC, or pass the `toolbarNotifications`
* wrapped in an object.
*/
const insertOrRemoveTooltip = async ({ toolbarNotifications }) => {
if (manualOverride) {
return
}
const isTooltipEnabled = await getTooltipState()
const isTooltipPresent = !!target
if (isTooltipEnabled && !isTooltipPresent) {
insertTooltip({ toolbarNotifications })
} else if (!isTooltipEnabled && isTooltipPresent) {
removeTooltip()
}
}
/**
* Sets up RPC functions to insert and remove Tooltip from Popup.
*/
export const setupRPC = ({ toolbarNotifications }) => {
makeRemotelyCallable({
showContentTooltip: async () => {
if (!showTooltip) {
await insertTooltip({ toolbarNotifications })
}
if (userSelectedText()) {
const position = calculateTooltipPostion()
showTooltip(position)
}
},
insertTooltip: ({ override } = {}) => {
manualOverride = !!override
insertTooltip({ toolbarNotifications })
},
removeTooltip: ({ override } = {}) => {
manualOverride = !!override
removeTooltip()
},
insertOrRemoveTooltip: async () => {
await insertOrRemoveTooltip({ toolbarNotifications })
},
})
}
/**
* Checks for certain conditions before triggering the tooltip.
* i) Whether the selection made by the user is just text.
* ii) Whether the selected target is not inside the tooltip itself.
*
* Event is undefined in the scenario of user selecting the text before the
* page has loaded. So we don't need to check for condition ii) since the
* tooltip wouldn't have popped up yet.
*/
export const conditionallyTriggerTooltip = delayed(
async ({ callback, toolbarNotifications }, event) => {
if (!userSelectedText() || (event && isTargetInsideTooltip(event))) {
return
}
/*
If all the conditions passed, then this returns the position to anchor the
tooltip. The positioning is based on the user's preferred method. But in the
case of tooltip popping up before page load, it resorts to text based method
*/
const positioning = await getPositionState()
let position
if (positioning === 'text' || !event) {
position = calculateTooltipPostion()
} else if (positioning === 'mouse' && event) {
position = { x: event.pageX, y: event.pageY }
}
callback(position)
conditionallyShowHighlightNotification({
toolbarNotifications,
position,
})
},
300,
)
export function calculateTooltipPostion() {
const range = document.getSelection().getRangeAt(0)
const boundingRect = range.getBoundingClientRect()
// x = position of element from the left + half of it's width
const x = boundingRect.left + boundingRect.width / 2
// y = scroll height from top + pixels from top + height of element - offset
const y = window.pageYOffset + boundingRect.top + boundingRect.height - 10
return {
x,
y,
}
}
function isAnchorOrContentEditable(selected) {
// Returns true if the any of the parent is an anchor element
// or is content editable.
let parent = selected.parentElement
while (parent) {
if (parent.contentEditable === 'true' || parent.nodeName === 'A') {
return true
}
parent = parent.parentElement
}
return false
}
export function userSelectedText() {
const selection = document.getSelection()
if (selection.type === 'None') {
return false
}
const selectedString = selection.toString().trim()
const container = selection.getRangeAt(0).commonAncestorContainer
const extras = isAnchorOrContentEditable(container)
const userSelectedText =
!!selection && !selection.isCollapsed && !!selectedString && !extras
return userSelectedText
}
function isTargetInsideTooltip(event) {
const $tooltipContainer = document.querySelector(
'#memex-direct-linking-tooltip',
)
if (!$tooltipContainer) {
// edge case, where the destroy() is called
return true
}
return $tooltipContainer.contains(event.target)
}