diff --git a/package.json b/package.json index 9dfeb1d..1c265e1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node app.js" + "start": "node app.js", + "dev": "webpack --watch --progress" }, "author": "", "license": "ISC", @@ -25,7 +26,7 @@ "jsdoc": "^3.6.2", "jsdoc-vuejs": "^3.0.0", "style-loader": "^0.23.1", - "vue-loader": "^15.7.0", + "vue-loader": "^15.9.6", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.6.10", "webpack": "^4.43.0", @@ -36,6 +37,8 @@ "@fortawesome/free-regular-svg-icons": "^5.8.1", "@fortawesome/free-solid-svg-icons": "^5.8.1", "@fortawesome/vue-fontawesome": "^0.1.6", + "@sentry/tracing": "^6.2.5", + "@sentry/vue": "^6.2.5", "axios": "^0.18.0", "express": "^4.17.0", "html-to-text": "^5.1.1", @@ -43,8 +46,10 @@ "quill-mention": "^2.1.1", "v-tooltip": "^2.0.1", "vue": "^2.6.10", + "vue-cli": "^2.9.6", "vue-jwt-decode": "^0.1.0", "vue-quill": "^1.5.1", - "vue-spinners": "^1.0.2" + "vue-spinners": "^1.0.2", + "webpack-dev-server": "^3.11.2" } } diff --git a/public/style/plugin.css b/public/style/plugin.css index 731dce1..16dde09 100644 --- a/public/style/plugin.css +++ b/public/style/plugin.css @@ -1,3 +1,6 @@ +@import url('https://fonts.googleapis.com/css2?family=Handlee&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display&display=swap'); + #nb-app { position: absolute; top: 0; @@ -11,6 +14,12 @@ font-style: italic; } +#ql-toolbar.ql-snow { + border: 1px solid #ccc; + box-sizing: border-box; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + padding: 2px; +} /* Temporary workaround with plugin inheritting parent's style ... Would be nice to use Shadow DOM instead once it's supported by Quill. @@ -102,7 +111,7 @@ } #nb-app .nb-highlights .nb-highlight { fill: rgb(255, 204, 1); - opacity: 0.2; + fill-opacity: 0.2; } #nb-app .nb-sidebar { @@ -320,7 +329,7 @@ background-color: #f0f0f0; } #nb-app .nb-sidebar .list-view .list-row .flags { - min-width: 50px; + /* width: 60px; */ display: flex; align-items: center; justify-content: space-between; @@ -358,6 +367,43 @@ height: 16px; } +#nb-app .nb-sidebar .list-view .list-row .flags .icon-wrapper.inno { + background-color: #4a2270; + color: #fff; + font-size: 14px; + font-family: 'Verdana', 'Geneva', sans-serif; +} +#nb-app .nb-sidebar .list-view .list-row .flags .placeholder.inno { + width: 16px; + height: 16px; +} + +#nb-app .nb-spotlight-control { + display: inline-flex; + vertical-align: middle; +} + + +#nb-app .nb-spotlight-control span { + background-color: #a67cce; + color: #fff; + font-size: 14px; + font-family: 'Verdana', 'Geneva', sans-serif; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 3px; + margin: 0 2px; + cursor: pointer; +} + +#nb-app .nb-spotlight-control span.active { + background-color: #4a2270; + cursor: not-allowed; +} + #nb-app .nb-sidebar .thread-view { min-height: 100px; margin-bottom: 10px; @@ -387,7 +433,7 @@ margin-bottom: 5px; } #nb-app .nb-sidebar .thread-view .thread-row .header .author { - font-size: 15px; + font-size: 12px; } #nb-app .nb-sidebar .thread-view .thread-row .header .author .instr-icon { display: inline-block; @@ -479,7 +525,7 @@ margin-top: auto; } #nb-app .editor-view .header { - font-size: 14px; + font-size: 10px; color: #444; padding-bottom: 5px; } @@ -502,7 +548,7 @@ width: 80px; padding: 6px; border-radius: 0px; - font-size: 14px; + font-size: 12px; color: #fff; cursor: pointer; } @@ -736,11 +782,144 @@ font-size: 11px; } nb-innotation { - border: 2px limegreen solid; - margin: 10px; - padding: 10px; + border-radius: 1px; + border:none; + margin: 2px; flex: 1; + padding: 20px 10px 10px 20px; + /* align-items: center; */ + display: flex; + text-align: left; + justify-content: left; + background-color: #fff5a4; + box-shadow: 0px 2px 5px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%); + box-sizing: border-box; + font-size: 0.8rem; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + transition: 0.5s; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + overflow: auto; + +} +nb-innotation:hover { + box-shadow: 5px 7px 9px 0px rgb(0 0 0 / 20%), 2px 1px 2px 1px rgb(0 0 0 / 14%), 1px 1px 10px 0px rgb(0 0 0 / 12%); + cursor: pointer; + transition: 0.5s; + background-color: #fff7b3; +} + +nb-innotation.active { + box-shadow: 0px -1px 16px 5px rgb(0 0 0 / 20%), 2px 1px 2px 1px rgb(0 0 0 / 14%), 1px 1px 10px 0px rgb(0 0 0 / 12%); + cursor: pointer; + transition: 0.5s; + background-color: #fff06e; +} + +nb-innotation-inline { + background: white; + margin: 0 5px; + font-family: 'Handlee', cursive; + color: blue; + cursor: pointer; + border: 3px white solid; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + position: relative; + z-index: 200; +} + +nb-innotation-collection { + display: flex; +} + +nb-innotation-collection.nb-above, +nb-innotation-collection.nb-bellow { + flex-direction: row; + width: 100%; +} + +nb-innotation-collection.nb-right, +nb-innotation-collection.nb-left { + flex-direction: column; + /* height: 100%; */ +} + +nb-innotation-collection.nb-above { + height: 120px; + position: absolute; + top: 0; + left: 0; +} + +nb-innotation-collection.nb-bellow { + height: 120px; + position: absolute; + bottom: 0; + left: 0; +} + +nb-innotation-collection.nb-right { + width: 180px; + position: absolute; + top: 0; + right: 0; +} + +nb-innotation-collection.nb-left { + width: 180px; + position: absolute; + top: 0; + left: 0; +} + +.nb-innotation-ancestor { + position: relative; +} + +.nb-innotation-ancestor.nb-above { + padding-top: 130px; +} + +.nb-innotation-ancestor.nb-bellow { + padding-bottom: 130px; +} + +.nb-innotation-ancestor.nb-right { + padding-right: 190px; +} + +.nb-innotation-ancestor.nb-left { + padding-left: 190px; +} + +nb-innotation-controller { + position: fixed; + display: flex; + height: 30px; + width: 171px; + background: #4a2270; + z-index: 99999999; + bottom: 0; + left: 41px; + color: white; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding: 10px; + box-shadow: 0px -4px 11px 0px rgb(143 141 143 / 72%); + font-size: 30px; + justify-content: center; + vertical-align: middle; } + .nb-innotation-wrapper { /* plan to make up and down part of this wrapper */ } @@ -756,4 +935,56 @@ nb-innotation { } .nb-innotation-wrapper .nb-common-ancestor { flex: 2; -} \ No newline at end of file +} + +#nb-app #nb-marginalias { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + padding: 0 10px; + top: 0; + right: 395px; + line-height: normal; + font-size: 12px; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + background: #fff; + z-index: 3000; + overflow: hidden; + position: absolute; +} + +#nb-app .nb-marginalia { + /* border-radius: 1px; + border:none; + margin: 2px; + padding: 20px 10px 10px 20px; + display: flex; + text-align: left; + justify-content: left; + background-color: #fff5a4; + box-shadow: 0px 2px 5px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%); + box-sizing: border-box; + font-size: 0.8rem; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + overflow: auto; */ + outline:none; + position: absolute; + font-family: 'Playfair Display', serif; + font-size: 12px; + padding: 10px; + display: flex; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: 0.3s; + cursor: pointer; + width: 180px; + -webkit-mask-image: linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5410539215686274) 30%, rgba(255,255,255,1) 100%); + mask-image: linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5410539215686274) 30%, rgba(255,255,255,1) 100%); + background-color: #ffffff; +} + diff --git a/src/app.js b/src/app.js index 1abca3d..802a5be 100644 --- a/src/app.js +++ b/src/app.js @@ -11,13 +11,27 @@ import { createNbRange, deserializeNbRange } from './models/nbrange.js' import NbComment from './models/nbcomment.js' import { isNodePartOf } from './utils/dom-util.js' +import NbInnotations from './components/spotlights/innotations/NbInnotations.vue' +import NbMarginalias from './components/spotlights/marginalias/NbMarginalias.vue' import NbHighlights from './components/highlights/NbHighlights.vue' import NbSidebar from './components/NbSidebar.vue' import NbNoAccess from './components/NbNoAccess.vue' import NbLogin from './components/NbLogin.vue' import axios from 'axios' import VueJwtDecode from "vue-jwt-decode"; +// import * as Sentry from "@sentry/vue"; +// import { Integrations } from "@sentry/tracing"; +// Sentry.init({ +// Vue, +// dsn: "https://0166e76b64ce48cf97d7df2b6d93ea90@o564291.ingest.sentry.io/5714967", +// integrations: [new Integrations.BrowserTracing()], + +// // Set tracesSampleRate to 1.0 to capture 100% +// // of transactions for performance monitoring. +// // We recommend adjusting this value in production +// tracesSampleRate: 1.0, +// }); Vue.use(VueQuill) Vue.use(VTooltip) @@ -25,14 +39,14 @@ Vue.use(VTooltip) Vue.component('font-awesome-icon', FontAwesomeIcon) library.add(fas, far) -axios.defaults.baseURL = 'https://nb2.csail.mit.edu/' +//axios.defaults.baseURL = 'https://nb2.csail.mit.edu/' // axios.defaults.baseURL = 'https://jumana-nb.csail.mit.edu/' -//axios.defaults.baseURL = 'https://127.0.0.1:3000/' // for local dev only +axios.defaults.baseURL = 'https://127.0.0.1:3000/' // for local dev only axios.defaults.withCredentials = true -export const PLUGIN_HOST_URL = 'https://nb2.csail.mit.edu/client' +// export const PLUGIN_HOST_URL = 'https://nb2.csail.mit.edu/client' // export const PLUGIN_HOST_URL = 'https://jumana-nb.csail.mit.edu/client' -// export const PLUGIN_HOST_URL = 'https://127.0.0.1:3001' // for local dev only +export const PLUGIN_HOST_URL = 'https://127.0.0.1:3001' // for local dev only if ( (document.attachEvent && document.readyState === 'complete') || @@ -125,6 +139,32 @@ function embedNbApp () {
+ + + + t.spotlight && ['ABOVE', 'BELLOW', 'LEFT', 'RIGHT'].includes(t.spotlight.type)) + }, + innotationsInline: function () { + return this.filteredThreads.filter(t => t.spotlight && t.spotlight.type === 'IN') + }, + marginalias: function () { + return this.filteredThreads.filter(t => t.spotlight && t.spotlight.type === 'MARGIN') + }, filteredThreads: function () { let items = this.threads let searchText = this.filter.searchText @@ -359,35 +423,78 @@ function embedNbApp () { }, activeClass: async function (newActiveClass) { if (newActiveClass != {} && this.user) { - let source = window.location.origin + window.location.pathname - if (this.sourceURL.length > 0) { - source = this.sourceURL - } - const token = localStorage.getItem("nb.user"); - const config = { headers: { Authorization: 'Bearer ' + token }, params: { url: source, class: newActiveClass.id } } - - axios.get('/api/annotations/allUsers', config) - .then(res => { - this.users = res.data - this.$set(this.user, 'role', this.users[this.user.id].role) - }) - - axios.get('/api/annotations/allTagTypes', config) - .then(res => { - this.hashtags = res.data - }) - - this.getAllAnnotations(source, newActiveClass) // another axios call put into a helper method + let source = window.location.origin + window.location.pathname + if (this.sourceURL.length > 0) { + source = this.sourceURL + } + const token = localStorage.getItem("nb.user"); + const config = { headers: { Authorization: 'Bearer ' + token }, params: { url: source, class: newActiveClass.id } } + const decoded = VueJwtDecode.decode(token) + + axios.get('/api/annotations/allUsers', config) + .then(res => { + this.users = res.data + this.$set(this.user, 'role', this.users[this.user.id].role) + + const configSessionStart = { headers: { Authorization: 'Bearer ' + token }, params: { url: this.sourceURL } } + axios.post(`/api/spotlights/log/session/start`, { + action: 'SESSION_START', + type: 'NONE', + class_id: this.activeClass.id, + role: this.users[this.user.id].role.toUpperCase() + }, configSessionStart) + }) + + axios.get('/api/annotations/allTagTypes', config) + .then(res => { + this.hashtags = res.data + }) + + this.getAllAnnotations(source, newActiveClass) // another axios call put into a helper method } } }, - created: function () { + created: async function () { + const req = await axios.get('/api/nb/config') + const configs = req.data + this.nbConfigs = configs + + this.isEmphasize = configs['SPOTLIGHT_EM'] === 'true' ? true : false + const token = localStorage.getItem("nb.user") if (token) { const decoded = VueJwtDecode.decode(token) this.user = decoded.user } + + if (document.location.href.includes('/nb_viewer.html')) { + this.isMarginalia = configs['SPOTLIGHT_MARGIN'] === 'true' ? true : false + this.isInnotation = false + } else { + this.isMarginalia = false + this.isInnotation = false //configs['SPOTLIGHT_INNOTATION'] === 'true' ? true : false + this.isEmphasize = false + } + + //TEMP remove NB2 on test + Array.from(document.getElementsByTagName("link")).forEach(elm => { + if (elm.href.includes("https://nb2.csail.mit.edu")){ + elm.remove() + } + }) + + Array.from(document.getElementsByTagName("script")).forEach(elm => { + if (elm.src.includes("https://nb2.csail.mit.edu")){ + elm.remove() + } + }) + + // remove hypothesis + const hypothesisSidebar = document.getElementsByTagName('hypothesis-sidebar') + const hypothesisAdder = document.getElementsByTagName('hypothesis-adder') + hypothesisSidebar && hypothesisSidebar[0] && hypothesisSidebar[0].remove() + hypothesisAdder && hypothesisAdder[0] && hypothesisAdder[0].remove() }, methods: { setUser: function (user) { @@ -613,22 +720,43 @@ function embedNbApp () { } this.filter.minUpvotes = min }, - onSelectThread: function (thread) { + onSelectThread: function (thread, threadViewInitiator='NONE') { + this.threadViewInitiator = threadViewInitiator + console.log('threadViewInitiator: ' + this.threadViewInitiator) this.threadSelected = thread thread.markSeenAll() }, onUnselectThread: function (thread) { + this.threadViewInitiator = 'NONE' + console.log('threadViewInitiator: ' + this.threadViewInitiator) + if (!this.isInnotationHover) { this.threadSelected = null - if (this.draftRange && this.isEditorEmpty) { - this.onCancelDraft() - } + } + if (this.draftRange && this.isEditorEmpty) { + this.onCancelDraft() + } }, onHoverThread: function (thread) { + // console.log('onHoverThread in app') + // console.log(thread) + if (!this.threadsHovered.includes(thread)) { + this.threadsHovered.push(thread) + } + }, + onHoverInnotation: function(thread) { + this.isInnotationHover = true if (!this.threadsHovered.includes(thread)) { this.threadsHovered.push(thread) } }, onUnhoverThread: function (thread) { + // console.log('onUnhoverThread in app') + // console.log(thread) + let idx = this.threadsHovered.indexOf(thread) + if (idx >= 0) this.threadsHovered.splice(idx, 1) + }, + onUnhoverInnotation: function(thread) { + this.isInnotationHover = false let idx = this.threadsHovered.indexOf(thread) if (idx >= 0) this.threadsHovered.splice(idx, 1) }, @@ -639,13 +767,22 @@ function embedNbApp () { this.resizeKey = Date.now() }, onSwitchClass: function(newClass) { - console.log('in app switch class'); - console.log(newClass); - - this.activeClass = newClass }, - onLogout: function () { + onSessionEnd: async function () { + if (this.activeClass.id){ + const token = localStorage.getItem("nb.user"); + const config = { headers: { Authorization: 'Bearer ' + token }, params: { url: this.sourceURL } } + await axios.post(`/api/spotlights/log/session/end`, { + action: 'SESSION_END', + type: 'NONE', + class_id: this.activeClass.id, + role: this.user.role.toUpperCase() + }, config) + } + }, + onLogout: async function () { + await this.onSessionEnd() localStorage.removeItem("nb.user") this.user = null this.myClasses = [] @@ -678,6 +815,8 @@ function embedNbApp () { } }, components: { + NbInnotations, + NbMarginalias, NbHighlights, NbSidebar, NbLogin, @@ -710,4 +849,20 @@ function embedNbApp () { window.addEventListener('resize', _ => { app.handleResize() }) + + window.addEventListener('scroll', _ => { + app.handleResize() + }) + + window.addEventListener('click', _ => { + app.handleResize() + }) + + window.addEventListener('beforeunload', async function (e) { + e.preventDefault() + await app.onSessionEnd() + return + }) + + } diff --git a/src/components/NbSidebar.vue b/src/components/NbSidebar.vue index 485ae75..0703097 100644 --- a/src/components/NbSidebar.vue +++ b/src/components/NbSidebar.vue @@ -35,6 +35,11 @@ :threads-hovered="threadsHovered" :show-highlights="showHighlights" :still-gathering-threads="stillGatheringThreads" + :is-marginalia="isMarginalia" + :is-emphasize="isEmphasize" + :is-innotation="isInnotation" + :activeClass="activeClass" + :user="user" @toggle-highlights="onToggleHighlights" @select-thread="onSelectThread" @hover-thread="onHoverThread" @@ -45,6 +50,11 @@ :thread="threadSelected" :me="user" :replyToComment="replyToComment" + :is-marginalia="isMarginalia" + :is-emphasize="isEmphasize" + :is-innotation="isInnotation" + :activeClass="activeClass" + :thread-view-initiator="threadViewInitiator" @edit-comment="onEditComment" @delete-comment="onDeleteComment" @draft-reply="onDraftReply"> @@ -111,6 +121,9 @@ export default { type: Boolean, default: true }, + isMarginalia: Boolean, + isInnotation: Boolean, + isEmphasize: Boolean, threads: { // threads after filter type: Object, default: () => {} @@ -128,7 +141,9 @@ export default { sourceUrl: { type: String, default: "" - } + }, + activeClass: Object, + threadViewInitiator: String, }, data () { return { @@ -238,8 +253,8 @@ export default { onMinUpvotes: function (min) { this.$emit('min-upvotes', min) }, - onSelectThread: function (thread) { - this.$emit('select-thread', thread) + onSelectThread: function (thread, threadViewInitiator='NONE') { + this.$emit('select-thread', thread, threadViewInitiator) }, onHoverThread: function (thread) { this.$emit('hover-thread', thread) @@ -307,8 +322,9 @@ export default { upvoteCount: 0, seenByMe: true }) + let source = this.sourceUrl.length > 0 ? this.sourceUrl : window.location.href.split('?')[0] - comment.submitAnnotation(this.activeClass.id, source) + comment.submitAnnotation(this.activeClass.id, source, this.threadViewInitiator, this.replyToComment, this.activeClass, this.user) if (this.draftRange) { this.$emit('new-thread', comment) diff --git a/src/components/highlights/NbHighlight.vue b/src/components/highlights/NbHighlight.vue index d6af60e..40ab115 100644 --- a/src/components/highlights/NbHighlight.vue +++ b/src/components/highlights/NbHighlight.vue @@ -3,7 +3,7 @@ class="nb-highlight" v-if="visible" :style="style" - @click="$emit('select-thread',thread)" + @click="onClick()" @mouseenter="onHover(true)" @mouseleave="onHover(false)"> import { getTextBoundingBoxes } from '../../utils/overlay-util.js' +import axios from 'axios' /** * Component for individual highlight overlay corresponding to each thread. @@ -69,6 +70,7 @@ export default { name: 'nb-highlight', props: { thread: Object, + user: Object, threadSelected: Object, threadsHovered: { type: Array, @@ -78,7 +80,13 @@ export default { showHighlights: { type: Boolean, default: true - } + }, + activeClass: { + type: Object, + default: () => {} + }, + isEmphasize: Boolean, + isInnotation: Boolean, }, watch: { /** @@ -86,6 +94,7 @@ export default { * in the view. If not, scroll down/up the window to center the highlight. */ threadSelected: function (val) { + //console.log('threadSelected nbH') if (this.thread !== val) { return } let rect = this.$el.getBoundingClientRect() let elTop = rect.top @@ -107,13 +116,16 @@ export default { computed: { style: function () { if (!this.thread) { - return 'fill: rgb(231, 76, 60); opacity: 0.3;' + return 'fill: rgb(231, 76, 60); fill-opacity: 0.3;' } if (this.thread === this.threadSelected) { - return 'fill: rgb(1, 99, 255); opacity: 0.3;' + return 'fill: rgb(1, 99, 255); fill-opacity: 0.3;' } if (this.threadsHovered.includes(this.thread)) { - return 'fill: rgb(1, 99, 255); opacity: 0.12;' + return 'fill: rgb(1, 99, 255); fill-opacity: 0.12;' + } + if (this.thread.spotlight && this.thread.spotlight.type === 'EM' && this.isEmphasize) { + return 'stroke: lime; fill: lime; fill-opacity: 0.3; stroke-opacity: 0.9; stroke-dasharray: 1,1; stroke-width: 2px;' } return null }, @@ -139,6 +151,38 @@ export default { methods: { onHover: function (state) { this.$emit(state ? 'hover-thread' : 'unhover-thread', this.thread) + }, + onClick: function () { + + if (!this.thread) { + return this.$emit('select-thread', this.thread, 'NONE') + } + + let type = 'HIGHLIGHT' + + if ((this.isEmphasize && this.thread.spotlight && this.thread.spotlight.type === 'EM') || (this.isInnotation && this.thread.spotlight && this.thread.spotlight.type === 'IN')) { + type = this.thread.spotlight.type.toUpperCase() + } + + const source = window.location.pathname === '/nb_viewer.html' ? window.location.href : window.location.origin + window.location.pathname + const token = localStorage.getItem("nb.user"); + const config = { headers: { Authorization: 'Bearer ' + token }, params: { url: source } } + axios.post(`/api/spotlights/log`, { + spotlight_id: type === 'HIGHLIGHT' ? null : this.thread.spotlight.id, + action: 'CLICK', + type: type, + annotation_id: this.thread.id, + class_id: this.activeClass.id, + role: this.user.role.toUpperCase() + }, config) + + if (this.isEmphasize && this.thread.spotlight && this.thread.spotlight.type === 'EM') { + this.$emit('select-thread', this.thread, 'SPOTLIGHT') + } else if (this.isInnotation && this.thread.spotlight && this.thread.spotlight.type === 'IN') { + this.$emit('select-thread', this.thread, 'SPOTLIGHT') + } else { + this.$emit('select-thread', this.thread, 'HIGHLIGHT') + } } } } diff --git a/src/components/highlights/NbHighlights.vue b/src/components/highlights/NbHighlights.vue index aaad653..53693c9 100644 --- a/src/components/highlights/NbHighlights.vue +++ b/src/components/highlights/NbHighlights.vue @@ -7,7 +7,11 @@ :thread-selected="threadSelected" :threads-hovered="threadsHovered" :show-highlights="showHighlights" - @select-thread="$emit('select-thread',thread)" + :user="user" + :activeClass="activeClass" + :is-emphasize="isEmphasize" + :is-innotation="isInnotation" + @select-thread="onSelectThread" @hover-thread="$emit('hover-thread',thread)" @unhover-thread="$emit('unhover-thread',thread)"> @@ -58,7 +62,19 @@ export default { showHighlights: { type: Boolean, default: true - } + }, + user: Object, + activeClass: { + type: Object, + default: () => {} + }, + isEmphasize: Boolean, + isInnotation: Boolean, + }, + methods: { + onSelectThread: function (thread, threadViewInitiator='NONE') { + this.$emit('select-thread', thread, threadViewInitiator) + }, }, mounted: function () { eventsProxyMouse(document.body, this.$el) diff --git a/src/components/list/ListRow.vue b/src/components/list/ListRow.vue index fad0765..a238bbb 100644 --- a/src/components/list/ListRow.vue +++ b/src/components/list/ListRow.vue @@ -1,33 +1,47 @@ diff --git a/src/components/spotlights/innotations/NbInnotationBlock.vue b/src/components/spotlights/innotations/NbInnotationBlock.vue new file mode 100644 index 0000000..34b8ae4 --- /dev/null +++ b/src/components/spotlights/innotations/NbInnotationBlock.vue @@ -0,0 +1,155 @@ + + + \ No newline at end of file diff --git a/src/components/spotlights/innotations/NbInnotationInline.vue b/src/components/spotlights/innotations/NbInnotationInline.vue new file mode 100644 index 0000000..8302b85 --- /dev/null +++ b/src/components/spotlights/innotations/NbInnotationInline.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/src/components/spotlights/innotations/NbInnotationPosition.js b/src/components/spotlights/innotations/NbInnotationPosition.js new file mode 100644 index 0000000..b7f6fc1 --- /dev/null +++ b/src/components/spotlights/innotations/NbInnotationPosition.js @@ -0,0 +1,6 @@ +export default { + TOP: 'top', + BOTTOM: 'bottom', + LEFT: 'left', + RIGHT: 'right' +} \ No newline at end of file diff --git a/src/components/spotlights/innotations/NbInnotations.vue b/src/components/spotlights/innotations/NbInnotations.vue new file mode 100644 index 0000000..1110512 --- /dev/null +++ b/src/components/spotlights/innotations/NbInnotations.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/src/components/spotlights/marginalias/NbMarginalia.vue b/src/components/spotlights/marginalias/NbMarginalia.vue new file mode 100644 index 0000000..aed3773 --- /dev/null +++ b/src/components/spotlights/marginalias/NbMarginalia.vue @@ -0,0 +1,98 @@ + + + \ No newline at end of file diff --git a/src/components/spotlights/marginalias/NbMarginalias.vue b/src/components/spotlights/marginalias/NbMarginalias.vue new file mode 100644 index 0000000..5cb0005 --- /dev/null +++ b/src/components/spotlights/marginalias/NbMarginalias.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/src/components/thread/ThreadComment.vue b/src/components/thread/ThreadComment.vue index 23c458c..dc4fba0 100644 --- a/src/components/thread/ThreadComment.vue +++ b/src/components/thread/ThreadComment.vue @@ -94,6 +94,8 @@ :me="me" :replyToComment="replyToComment" :key="child.id" + :activeClass="activeClass" + :thread-view-initiator="threadViewInitiator" @edit-comment="editComment" @delete-comment="deleteComment" @draft-reply="draftReply" @@ -132,7 +134,9 @@ export default { props: { comment: Object, me: Object, - replyToComment: Object + replyToComment: Object, + activeClass: Object, + threadViewInitiator: String, }, data () { return { @@ -166,13 +170,13 @@ export default { this.$emit('draft-reply', comment) }, toggleBookmark: function (comment) { - comment.toggleBookmark() + comment.toggleBookmark(this.threadViewInitiator, this.comment, this.activeClass, this.me) }, toggleUpvote: function (comment) { - comment.toggleUpvote() + comment.toggleUpvote(this.threadViewInitiator, this.comment, this.activeClass, this.me) }, toggleReplyRequest: function (comment) { - comment.toggleReplyRequest() + comment.toggleReplyRequest(this.threadViewInitiator, this.comment, this.activeClass, this.me) } }, computed: { diff --git a/src/components/thread/ThreadView.vue b/src/components/thread/ThreadView.vue index e149668..a91c629 100644 --- a/src/components/thread/ThreadView.vue +++ b/src/components/thread/ThreadView.vue @@ -6,11 +6,21 @@ {{ thread.countAllReplyReqs() }} +  ·  + +
@@ -20,6 +30,7 @@ diff --git a/src/models/nbcomment.js b/src/models/nbcomment.js index b9231ba..4dcb21b 100644 --- a/src/models/nbcomment.js +++ b/src/models/nbcomment.js @@ -26,6 +26,7 @@ class NbComment { * @param {Number} data.starCount - total upvotes for this comment, sets {@link NbComment#upvoteCount} * @param {Boolean} data.seenByMe - true if the current user's seen this comment, sets {@link NbComment#seenByMe} * @param {Boolean} data.bookmarked - true if the current user bookmarked this comment, sets {@link NbComment#bookmarked} + * @param {Object} data.spotlight */ constructor (data, annotationsData) { /** @@ -188,6 +189,8 @@ class NbComment { * @type Number */ this.setText() + + this.spotlight = data.spotlight } /** @@ -215,7 +218,7 @@ class NbComment { * On success, set {@link NbComment#id}. If this is a thread head, * {@link NbComment#loadReplies} will be called to also load replies. */ - submitAnnotation (classId, sourceUrl) { + submitAnnotation (classId, sourceUrl, threadViewInitiator='NONE', thread={}, activeClass={}, user={}) { const token = localStorage.getItem("nb.user"); const headers = { headers: { Authorization: 'Bearer ' + token }} if (!this.parent) { @@ -249,6 +252,7 @@ class NbComment { bookmark: this.bookmarked }, headers).then(res => { this.id = res.data.id + this.logSpotlightAction('REPLY', thread, activeClass, user, threadViewInitiator) }) } } @@ -419,6 +423,13 @@ class NbComment { return false } + isSpotlighted() { + if (this.spotlight) { + return true + } + return false + } + /** * Check recursively if this comment (or descendant) is authored by the user * defined by the given user ID. @@ -524,13 +535,15 @@ class NbComment { /** * Toggle the upvote for this comment by the current user. */ - toggleUpvote () { + toggleUpvote (threadViewInitiator='NONE', thread={}, activeClass={}, user={}) { if (this.upvotedByMe) { this.upvoteCount -= 1 this.upvotedByMe = false } else { this.upvoteCount += 1 this.upvotedByMe = true + + this.logSpotlightAction('STAR', thread, activeClass, user, threadViewInitiator) } if (this.id) { const token = localStorage.getItem("nb.user"); @@ -542,13 +555,15 @@ class NbComment { /** * Toggle the reply request for this comment by the current user. */ - toggleReplyRequest () { + toggleReplyRequest (threadViewInitiator='NONE', thread={}, activeClass={}, user={}) { if (this.replyRequestedByMe) { this.replyRequestCount -= 1 this.replyRequestedByMe = false } else { this.replyRequestCount += 1 this.replyRequestedByMe = true + + this.logSpotlightAction('REPLY_REQUEST', thread, activeClass, user, threadViewInitiator) } if (this.id) { const token = localStorage.getItem("nb.user"); @@ -560,7 +575,11 @@ class NbComment { /** * Toggle the bookmark for this comment by the current user. */ - toggleBookmark () { + toggleBookmark (threadViewInitiator='NONE', thread={}, activeClass={}, user={}) { + if (!this.bookmarked) { + this.logSpotlightAction('BOOKMARK', thread, activeClass, user, threadViewInitiator) + } + this.bookmarked = !this.bookmarked if (this.id) { const token = localStorage.getItem("nb.user"); @@ -569,6 +588,26 @@ class NbComment { } } + logSpotlightAction(action, comment, activeClass, user, threadViewInitiator) { + const source = window.location.pathname === '/nb_viewer.html' ? window.location.href : window.location.origin + window.location.pathname + const token = localStorage.getItem("nb.user"); + const config = { headers: { Authorization: 'Bearer ' + token }, params: { url: source } } + const headComment = this.getHeadComment(comment) + axios.post(`/api/spotlights/log`, { + spotlight_id: threadViewInitiator !== 'SPOTLIGHT' ? null : headComment.spotlight.id, + action: action.toUpperCase(), + type: threadViewInitiator !== 'SPOTLIGHT' ? threadViewInitiator : headComment.spotlight.type.toUpperCase(), + annotation_id: headComment.id, + class_id: activeClass.id, + role: user.role.toUpperCase() + }, config) + } + + getHeadComment(comment) { + if (!comment.parent) return comment + return this.getHeadComment(comment.parent) + } + /** * Update the content and settings of this comment and save changes to the backend. *