Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2020-01-01 16:39:08 +0300
committerJulius Härtl <jus@bitgrid.net>2021-01-13 21:34:01 +0300
commitcc654982b296a3869191800f3c7c5106d8275b08 (patch)
tree25ba2881ebe89dfe761cc8093e2ccebd04ebf399 /src/extensions
parent287d438b291de2b208f5379e92168bd16f9f5e02 (diff)
First throw on editor colors based on change tracking example of prosemirror
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'src/extensions')
-rw-r--r--src/extensions/UserColor.js224
-rw-r--r--src/extensions/index.js2
2 files changed, 226 insertions, 0 deletions
diff --git a/src/extensions/UserColor.js b/src/extensions/UserColor.js
new file mode 100644
index 000000000..344c08f79
--- /dev/null
+++ b/src/extensions/UserColor.js
@@ -0,0 +1,224 @@
+/*
+ * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import { Extension, Plugin } from 'tiptap'
+import { Decoration, DecorationSet } from 'prosemirror-view'
+
+const contrastRatio = (value) => {
+ const hexCode = value.charAt(0) === '#'
+ ? value.substr(1, 6)
+ : value
+
+ const hexR = parseInt(hexCode.substr(0, 2), 16)
+ const hexG = parseInt(hexCode.substr(2, 2), 16)
+ const hexB = parseInt(hexCode.substr(4, 2), 16)
+ return (hexR + hexG + hexB) / (255 * 3)
+}
+function updateBlameMap(map, transform, id) {
+ const result = []; const mapping = transform.mapping
+ for (let i = 0; i < map.length; i++) {
+ const span = map[i]
+ const from = mapping.map(span.from, 1); const to = mapping.map(span.to, -1)
+ if (from < to) result.push(new Span(from, to, span.commit))
+ }
+
+ for (let i = 0; i < mapping.maps.length; i++) {
+ const map = mapping.maps[i]; const after = mapping.slice(i + 1)
+ map.forEach((_s, _e, start, end) => {
+ insertIntoBlameMap(result, after.map(start, 1), after.map(end, -1), id)
+ })
+ }
+
+ return result
+}
+
+function insertIntoBlameMap(map, from, to, commit) {
+ if (from >= to) {
+ return
+ }
+ let pos = 0
+ let next
+ for (; pos < map.length; pos++) {
+ next = map[pos]
+ if (next.commit === commit) {
+ if (next.to >= from) break
+ } else if (next.to > from) { // Different commit, not before
+ if (next.from < from) { // Sticks out to the left (loop below will handle right side)
+ const left = new Span(next.from, from, next.commit)
+ if (next.to > to) map.splice(pos++, 0, left)
+ else map[pos++] = left
+ }
+ break
+ }
+ }
+
+ // eslint-ignore
+ while ((next = map[pos])) {
+ if (next.commit === commit) {
+ if (next.from > to) break
+ from = Math.min(from, next.from)
+ to = Math.max(to, next.to)
+ map.splice(pos, 1)
+ } else {
+ if (next.from >= to) break
+ if (next.to > to) {
+ map[pos] = new Span(to, next.to, next.commit)
+ break
+ } else {
+ map.splice(pos, 1)
+ }
+ }
+ }
+
+ map.splice(pos, 0, new Span(from, to, commit))
+}
+
+class TrackState {
+
+ constructor(blameMap, commits, uncommittedSteps, uncommittedMaps) {
+ // The blame map is a data structure that lists a sequence of
+ // document ranges, along with the commit that inserted them. This
+ // can be used to, for example, highlight the part of the document
+ // that was inserted by a commit.
+ this.blameMap = blameMap
+ // The commit history, as an array of objects.
+ this.commits = commits
+ // Inverted steps and their maps corresponding to the changes that
+ // have been made since the last commit.
+ this.uncommittedSteps = uncommittedSteps
+ this.uncommittedMaps = uncommittedMaps
+ }
+
+ // Apply a transform to this state
+ applyTransform(transform) {
+ // Invert the steps in the transaction, to be able to save them in
+ // the next commit
+ const inverted
+ = transform.steps.map((step, i) => step.invert(transform.docs[i]))
+ const newBlame = updateBlameMap(this.blameMap, transform, this.commits.length)
+ // Create a new state—since these are part of the editor state, a
+ // persistent data structure, they must not be mutated.
+ return new TrackState(newBlame, this.commits,
+ this.uncommittedSteps.concat(inverted),
+ this.uncommittedMaps.concat(transform.mapping.maps))
+ }
+
+ // When a transaction is marked as a commit, this is used to put any
+ // uncommitted steps into a new commit.
+ applyCommit(message, time, author) {
+ if (this.uncommittedSteps.length === 0) return this
+ const commit = new Commit(message, time, this.uncommittedSteps, this.uncommittedMaps, author)
+ return new TrackState(this.blameMap, this.commits.concat(commit), [], [])
+ }
+
+}
+
+class Span {
+
+ constructor(from, to, commit) {
+ this.from = from; this.to = to; this.commit = commit
+ }
+
+}
+
+class Commit {
+
+ constructor(message, time, steps, maps, author) {
+ this.message = message
+ this.time = time
+ this.steps = steps
+ this.maps = maps
+ this.author = author
+ }
+
+}
+
+class UserColor extends Extension {
+
+ get name() {
+ return 'users'
+ }
+
+ get defaultOptions() {
+ return {
+ clientID: 0,
+ color: (clientID) => {
+ return '#' + Math.floor((Math.abs(Math.sin(clientID) * 16777215)) % 16777215).toString(16) + 'aa'
+ },
+ name: (clientID) => {
+ return null
+ },
+ }
+ }
+
+ get plugins() {
+ return [
+ new Plugin({
+ clientID: this.options.clientID,
+ color: this.options.color,
+ name: this.options.name,
+ state: {
+ init(_, instance) {
+ return {
+ tracked: new TrackState([new Span(0, instance.doc.content.size, null)], [], [], []),
+ deco: DecorationSet.empty,
+ }
+ },
+ apply(tr, instance, oldState, state) {
+ let { tracked, decos } = instance
+ let tState = this.getState(oldState).tracked
+ if (tr.docChanged) {
+ tracked = tracked.applyTransform(tr)
+ const clientID = tr.getMeta('clientID') ? tr.getMeta('clientID') : this.spec.clientID
+ tracked = tracked.applyCommit(clientID, new Date(tr.time), {
+ clientID,
+ color: this.spec.color(clientID),
+ name: this.spec.name(clientID),
+ })
+ tState = tracked
+ }
+
+ decos = tState.blameMap
+ .filter(span => typeof tState.commits[span.commit] !== 'undefined')
+ .map(span => {
+ const commit = tState.commits[span.commit]
+ return Decoration.inline(span.from, span.to, {
+ class: 'author-annotation',
+ style: 'background-color: ' + commit.author.color + '; color:' + (contrastRatio(commit.author.color) > 0.4 ? '#fff' : '#000'),
+ title: commit.author.name,
+ })
+ })
+ return { tracked, deco: DecorationSet.create(state.doc, decos) }
+ },
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state).deco
+ },
+ },
+ }),
+ ]
+ }
+
+}
+
+export default UserColor
diff --git a/src/extensions/index.js b/src/extensions/index.js
index c50468fb9..856c8bd4e 100644
--- a/src/extensions/index.js
+++ b/src/extensions/index.js
@@ -21,7 +21,9 @@
*/
import Keymap from './Keymap'
+import UserColor from './UserColor'
export {
Keymap,
+ UserColor,
}