/** * yjs - A framework for real-time p2p shared editing on any data * @version v12.3.1 * @link http://y-js.org * @license MIT */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.yText = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o text2.length ? text1 : text2; var shorttext = text1.length > text2.length ? text2 : text1; var i = longtext.indexOf(shorttext); if (i != -1) { // Shorter text is inside the longer text (speedup). diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; // Swap insertions for deletions if diff is reversed. if (text1.length > text2.length) { diffs[0][0] = diffs[2][0] = DIFF_DELETE; } return diffs; } if (shorttext.length == 1) { // Single character string. // After the previous speedup, the character can't be an equality. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; } // Check to see if the problem can be split in two. var hm = diff_halfMatch_(text1, text2); if (hm) { // A half-match was found, sort out the return data. var text1_a = hm[0]; var text1_b = hm[1]; var text2_a = hm[2]; var text2_b = hm[3]; var mid_common = hm[4]; // Send both pairs off for separate processing. var diffs_a = diff_main(text1_a, text2_a); var diffs_b = diff_main(text1_b, text2_b); // Merge the results. return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); } return diff_bisect_(text1, text2); }; /** * Find the 'middle snake' of a diff, split the problem in two * and return the recursively constructed diff. * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @return {Array} Array of diff tuples. * @private */ function diff_bisect_(text1, text2) { // Cache the text lengths to prevent multiple calls. var text1_length = text1.length; var text2_length = text2.length; var max_d = Math.ceil((text1_length + text2_length) / 2); var v_offset = max_d; var v_length = 2 * max_d; var v1 = new Array(v_length); var v2 = new Array(v_length); // Setting all elements to -1 is faster in Chrome & Firefox than mixing // integers and undefined. for (var x = 0; x < v_length; x++) { v1[x] = -1; v2[x] = -1; } v1[v_offset + 1] = 0; v2[v_offset + 1] = 0; var delta = text1_length - text2_length; // If the total number of characters is odd, then the front path will collide // with the reverse path. var front = (delta % 2 != 0); // Offsets for start and end of k loop. // Prevents mapping of space beyond the grid. var k1start = 0; var k1end = 0; var k2start = 0; var k2end = 0; for (var d = 0; d < max_d; d++) { // Walk the front path one step. for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { var k1_offset = v_offset + k1; var x1; if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { x1 = v1[k1_offset + 1]; } else { x1 = v1[k1_offset - 1] + 1; } var y1 = x1 - k1; while (x1 < text1_length && y1 < text2_length && text1.charAt(x1) == text2.charAt(y1)) { x1++; y1++; } v1[k1_offset] = x1; if (x1 > text1_length) { // Ran off the right of the graph. k1end += 2; } else if (y1 > text2_length) { // Ran off the bottom of the graph. k1start += 2; } else if (front) { var k2_offset = v_offset + delta - k1; if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { // Mirror x2 onto top-left coordinate system. var x2 = text1_length - v2[k2_offset]; if (x1 >= x2) { // Overlap detected. return diff_bisectSplit_(text1, text2, x1, y1); } } } } // Walk the reverse path one step. for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { var k2_offset = v_offset + k2; var x2; if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { x2 = v2[k2_offset + 1]; } else { x2 = v2[k2_offset - 1] + 1; } var y2 = x2 - k2; while (x2 < text1_length && y2 < text2_length && text1.charAt(text1_length - x2 - 1) == text2.charAt(text2_length - y2 - 1)) { x2++; y2++; } v2[k2_offset] = x2; if (x2 > text1_length) { // Ran off the left of the graph. k2end += 2; } else if (y2 > text2_length) { // Ran off the top of the graph. k2start += 2; } else if (!front) { var k1_offset = v_offset + delta - k2; if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { var x1 = v1[k1_offset]; var y1 = v_offset + x1 - k1_offset; // Mirror x2 onto top-left coordinate system. x2 = text1_length - x2; if (x1 >= x2) { // Overlap detected. return diff_bisectSplit_(text1, text2, x1, y1); } } } } } // Diff took too long and hit the deadline or // number of diffs equals number of characters, no commonality at all. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; }; /** * Given the location of the 'middle snake', split the diff in two parts * and recurse. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} x Index of split point in text1. * @param {number} y Index of split point in text2. * @return {Array} Array of diff tuples. */ function diff_bisectSplit_(text1, text2, x, y) { var text1a = text1.substring(0, x); var text2a = text2.substring(0, y); var text1b = text1.substring(x); var text2b = text2.substring(y); // Compute both diffs serially. var diffs = diff_main(text1a, text2a); var diffsb = diff_main(text1b, text2b); return diffs.concat(diffsb); }; /** * Determine the common prefix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the start of each * string. */ function diff_commonPrefix(text1, text2) { // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { return 0; } // Binary search. // Performance analysis: http://neil.fraser.name/news/2007/10/09/ var pointermin = 0; var pointermax = Math.min(text1.length, text2.length); var pointermid = pointermax; var pointerstart = 0; while (pointermin < pointermid) { if (text1.substring(pointerstart, pointermid) == text2.substring(pointerstart, pointermid)) { pointermin = pointermid; pointerstart = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Determine the common suffix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of each string. */ function diff_commonSuffix(text1, text2) { // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { return 0; } // Binary search. // Performance analysis: http://neil.fraser.name/news/2007/10/09/ var pointermin = 0; var pointermax = Math.min(text1.length, text2.length); var pointermid = pointermax; var pointerend = 0; while (pointermin < pointermid) { if (text1.substring(text1.length - pointermid, text1.length - pointerend) == text2.substring(text2.length - pointermid, text2.length - pointerend)) { pointermin = pointermid; pointerend = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Do the two texts share a substring which is at least half the length of the * longer text? * This speedup can produce non-minimal diffs. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {Array.} Five element Array, containing the prefix of * text1, the suffix of text1, the prefix of text2, the suffix of * text2 and the common middle. Or null if there was no match. */ function diff_halfMatch_(text1, text2) { var longtext = text1.length > text2.length ? text1 : text2; var shorttext = text1.length > text2.length ? text2 : text1; if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { return null; // Pointless. } /** * Does a substring of shorttext exist within longtext such that the substring * is at least half the length of longtext? * Closure, but does not reference any external variables. * @param {string} longtext Longer string. * @param {string} shorttext Shorter string. * @param {number} i Start index of quarter length substring within longtext. * @return {Array.} Five element Array, containing the prefix of * longtext, the suffix of longtext, the prefix of shorttext, the suffix * of shorttext and the common middle. Or null if there was no match. * @private */ function diff_halfMatchI_(longtext, shorttext, i) { // Start with a 1/4 length substring at position i as a seed. var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); var j = -1; var best_common = ''; var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; while ((j = shorttext.indexOf(seed, j + 1)) != -1) { var prefixLength = diff_commonPrefix(longtext.substring(i), shorttext.substring(j)); var suffixLength = diff_commonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); if (best_common.length < suffixLength + prefixLength) { best_common = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); best_longtext_a = longtext.substring(0, i - suffixLength); best_longtext_b = longtext.substring(i + prefixLength); best_shorttext_a = shorttext.substring(0, j - suffixLength); best_shorttext_b = shorttext.substring(j + prefixLength); } } if (best_common.length * 2 >= longtext.length) { return [best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common]; } else { return null; } } // First check if the second quarter is the seed for a half-match. var hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4)); // Check again based on the third quarter. var hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2)); var hm; if (!hm1 && !hm2) { return null; } else if (!hm2) { hm = hm1; } else if (!hm1) { hm = hm2; } else { // Both matched. Select the longest. hm = hm1[4].length > hm2[4].length ? hm1 : hm2; } // A half-match was found, sort out the return data. var text1_a, text1_b, text2_a, text2_b; if (text1.length > text2.length) { text1_a = hm[0]; text1_b = hm[1]; text2_a = hm[2]; text2_b = hm[3]; } else { text2_a = hm[0]; text2_b = hm[1]; text1_a = hm[2]; text1_b = hm[3]; } var mid_common = hm[4]; return [text1_a, text1_b, text2_a, text2_b, mid_common]; }; /** * Reorder and merge like edit sections. Merge equalities. * Any edit section can move as long as it doesn't cross an equality. * @param {Array} diffs Array of diff tuples. */ function diff_cleanupMerge(diffs) { diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. var pointer = 0; var count_delete = 0; var count_insert = 0; var text_delete = ''; var text_insert = ''; var commonlength; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: count_insert++; text_insert += diffs[pointer][1]; pointer++; break; case DIFF_DELETE: count_delete++; text_delete += diffs[pointer][1]; pointer++; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if (count_delete + count_insert > 1) { if (count_delete !== 0 && count_insert !== 0) { // Factor out any common prefixies. commonlength = diff_commonPrefix(text_insert, text_delete); if (commonlength !== 0) { if ((pointer - count_delete - count_insert) > 0 && diffs[pointer - count_delete - count_insert - 1][0] == DIFF_EQUAL) { diffs[pointer - count_delete - count_insert - 1][1] += text_insert.substring(0, commonlength); } else { diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]); pointer++; } text_insert = text_insert.substring(commonlength); text_delete = text_delete.substring(commonlength); } // Factor out any common suffixies. commonlength = diff_commonSuffix(text_insert, text_delete); if (commonlength !== 0) { diffs[pointer][1] = text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1]; text_insert = text_insert.substring(0, text_insert.length - commonlength); text_delete = text_delete.substring(0, text_delete.length - commonlength); } } // Delete the offending records and add the merged ones. if (count_delete === 0) { diffs.splice(pointer - count_insert, count_delete + count_insert, [DIFF_INSERT, text_insert]); } else if (count_insert === 0) { diffs.splice(pointer - count_delete, count_delete + count_insert, [DIFF_DELETE, text_delete]); } else { diffs.splice(pointer - count_delete - count_insert, count_delete + count_insert, [DIFF_DELETE, text_delete], [DIFF_INSERT, text_insert]); } pointer = pointer - count_delete - count_insert + (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { // Merge this equality with the previous one. diffs[pointer - 1][1] += diffs[pointer][1]; diffs.splice(pointer, 1); } else { pointer++; } count_insert = 0; count_delete = 0; text_delete = ''; text_insert = ''; break; } } if (diffs[diffs.length - 1][1] === '') { diffs.pop(); // Remove the dummy entry at the end. } // Second pass: look for single edits surrounded on both sides by equalities // which can be shifted sideways to eliminate an equality. // e.g: ABAC -> ABAC var changes = false; pointer = 1; // Intentionally ignore the first and last element (don't need checking). while (pointer < diffs.length - 1) { if (diffs[pointer - 1][0] == DIFF_EQUAL && diffs[pointer + 1][0] == DIFF_EQUAL) { // This is a single edit surrounded by equalities. if (diffs[pointer][1].substring(diffs[pointer][1].length - diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { // Shift the edit over the previous equality. diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; diffs.splice(pointer - 1, 1); changes = true; } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == diffs[pointer + 1][1]) { // Shift the edit over the next equality. diffs[pointer - 1][1] += diffs[pointer + 1][1]; diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; diffs.splice(pointer + 1, 1); changes = true; } } pointer++; } // If shifts were made, the diff needs reordering and another shift sweep. if (changes) { diff_cleanupMerge(diffs); } }; var diff = diff_main; diff.INSERT = DIFF_INSERT; diff.DELETE = DIFF_DELETE; diff.EQUAL = DIFF_EQUAL; module.exports = diff; /* * Modify a diff such that the cursor position points to the start of a change: * E.g. * cursor_normalize_diff([[DIFF_EQUAL, 'abc']], 1) * => [1, [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'bc']]] * cursor_normalize_diff([[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xyz']], 2) * => [2, [[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xy'], [DIFF_DELETE, 'z']]] * * @param {Array} diffs Array of diff tuples * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds! * @return {Array} A tuple [cursor location in the modified diff, modified diff] */ function cursor_normalize_diff (diffs, cursor_pos) { if (cursor_pos === 0) { return [DIFF_EQUAL, diffs]; } for (var current_pos = 0, i = 0; i < diffs.length; i++) { var d = diffs[i]; if (d[0] === DIFF_DELETE || d[0] === DIFF_EQUAL) { var next_pos = current_pos + d[1].length; if (cursor_pos === next_pos) { return [i + 1, diffs]; } else if (cursor_pos < next_pos) { // copy to prevent side effects diffs = diffs.slice(); // split d into two diff changes var split_pos = cursor_pos - current_pos; var d_left = [d[0], d[1].slice(0, split_pos)]; var d_right = [d[0], d[1].slice(split_pos)]; diffs.splice(i, 1, d_left, d_right); return [i + 1, diffs]; } else { current_pos = next_pos; } } } throw new Error('cursor_pos is out of bounds!') } /* * Modify a diff such that the edit position is "shifted" to the proposed edit location (cursor_position). * * Case 1) * Check if a naive shift is possible: * [0, X], [ 1, Y] -> [ 1, Y], [0, X] (if X + Y === Y + X) * [0, X], [-1, Y] -> [-1, Y], [0, X] (if X + Y === Y + X) - holds same result * Case 2) * Check if the following shifts are possible: * [0, 'pre'], [ 1, 'prefix'] -> [ 1, 'pre'], [0, 'pre'], [ 1, 'fix'] * [0, 'pre'], [-1, 'prefix'] -> [-1, 'pre'], [0, 'pre'], [-1, 'fix'] * ^ ^ * d d_next * * @param {Array} diffs Array of diff tuples * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds! * @return {Array} Array of diff tuples */ function fix_cursor (diffs, cursor_pos) { var norm = cursor_normalize_diff(diffs, cursor_pos); var ndiffs = norm[1]; var cursor_pointer = norm[0]; var d = ndiffs[cursor_pointer]; var d_next = ndiffs[cursor_pointer + 1]; if (d == null) { // Text was deleted from end of original string, // cursor is now out of bounds in new string return diffs; } else if (d[0] !== DIFF_EQUAL) { // A modification happened at the cursor location. // This is the expected outcome, so we can return the original diff. return diffs; } else { if (d_next != null && d[1] + d_next[1] === d_next[1] + d[1]) { // Case 1) // It is possible to perform a naive shift ndiffs.splice(cursor_pointer, 2, d_next, d) return merge_tuples(ndiffs, cursor_pointer, 2) } else if (d_next != null && d_next[1].indexOf(d[1]) === 0) { // Case 2) // d[1] is a prefix of d_next[1] // We can assume that d_next[0] !== 0, since d[0] === 0 // Shift edit locations.. ndiffs.splice(cursor_pointer, 2, [d_next[0], d[1]], [0, d[1]]); var suffix = d_next[1].slice(d[1].length); if (suffix.length > 0) { ndiffs.splice(cursor_pointer + 2, 0, [d_next[0], suffix]); } return merge_tuples(ndiffs, cursor_pointer, 3) } else { // Not possible to perform any modification return diffs; } } } /* * Try to merge tuples with their neigbors in a given range. * E.g. [0, 'a'], [0, 'b'] -> [0, 'ab'] * * @param {Array} diffs Array of diff tuples. * @param {Int} start Position of the first element to merge (diffs[start] is also merged with diffs[start - 1]). * @param {Int} length Number of consecutive elements to check. * @return {Array} Array of merged diff tuples. */ function merge_tuples (diffs, start, length) { // Check from (start-1) to (start+length). for (var i = start + length - 1; i >= 0 && i >= start - 1; i--) { if (i + 1 < diffs.length) { var left_d = diffs[i]; var right_d = diffs[i+1]; if (left_d[0] === right_d[1]) { diffs.splice(i, 2, [left_d[0], left_d[1] + right_d[1]]); } } } return diffs; } },{}],2:[function(require,module,exports){ /* global Y, Element */ 'use strict' var diff = require('fast-diff') var monacoIdentifierTemplate = { major: 0, minor: 0 } function extend (Y) { Y.requestModules(['Array']).then(function () { class YText extends Y.Array.typeDefinition['class'] { constructor (os, _model, _content) { super(os, _model, _content) this.textfields = [] this.aceInstances = [] this.codeMirrorInstances = [] this.monacoInstances = [] } toString () { return this._content.map(function (c) { return c.val }).join('') } insert (pos, content) { var arr = content.split('') for (var i = 0; i < arr.length; i++) { if (/[\uD800-\uDFFF]/.test(arr[i])) { // is surrogate pair arr[i] = arr[i] + arr[i + 1] arr[i + 1] = '' i++ } } super.insert(pos, arr) } delete (pos, length) { if (length == null) { length = 1 } if (typeof length !== 'number') { throw new Error('length must be a number!') } if (typeof pos !== 'number') { throw new Error('pos must be a number!') } if (pos + length > this._content.length || pos < 0 || length < 0) { throw new Error('The deletion range exceeds the range of the array!') } if (length === 0) { return } // This is for the case that part of a surrogate pair is deleted // we store surrogate pairs like this: [.., '🐇', '', ..] (string, code) if (this._content.length > pos + length && this._content[pos + length].val === '' && this._content[pos + length - 1].val.length === 2) { // case one. first part of the surrogate pair is deleted let token = this._content[pos + length - 1].val[0] super.delete(pos, length + 1) super.insert(pos, [token]) } else if (pos > 0 && this._content[pos].val === '' && this._content[pos - 1].val.length === 2) { // case two. second part of the surrogate pair is deleted let token = this._content[pos - 1].val[1] super.delete(pos - 1, length + 1) super.insert(pos - 1, [token]) } else { super.delete(pos, length) } } unbindAll () { this.unbindTextareaAll() this.unbindAceAll() this.unbindCodeMirrorAll() this.unbindMonacoAll() } // Monaco implementation unbindMonaco (monacoInstance) { var i = this.monacoInstances.findIndex(function (binding) { return binding.editor === monacoInstance }) if (i >= 0) { var binding = this.monacoInstances[i] this.unobserve(binding.yCallback) binding.disposeBinding() this.monacoInstances.splice(i, 1) } } unbindMonacoAll () { for (let i = this.monacoInstances.length - 1; i >= 0; i--) { this.unbindMonaco(this.monacoInstances[i].editor) } } bindMonaco (monacoInstance, options) { var self = this options = options || {} // this function makes sure that either the // monaco event is executed, or the yjs observer is executed var token = true function mutualExcluse (f) { if (token) { token = false try { f() } catch (e) { token = true throw new Error(e) } token = true } } monacoInstance.setValue(this.toString()) function monacoCallback (event) { mutualExcluse(function () { // compute start.. (col+row -> index position) // We shouldn't compute the offset on the old model.. // var start = monacoInstance.model.getOffsetAt({column: event.range.startColumn, lineNumber: event.range.startLineNumber}) // So we compute the offset using the _content of this type for (var i = 0, line = 1; line < event.range.startLineNumber; i++) { if (self._content[i].val === '\n') { line++ } } var start = i + event.range.startColumn - 1 // apply the delete operation first if (event.rangeLength > 0) { self.delete(start, event.rangeLength) } // apply insert operation self.insert(start, event.text) }) } var disposeBinding = monacoInstance.onDidChangeModelContent(monacoCallback).dispose function yCallback (event) { mutualExcluse(function () { let start = monacoInstance.model.getPositionAt(event.index) var end, text if (event.type === 'insert') { end = start text = event.values.join('') } else if (event.type === 'delete') { end = monacoInstance.model.modifyPosition(start, event.length) text = '' } var range = { startLineNumber: start.lineNumber, startColumn: start.column, endLineNumber: end.lineNumber, endColumn: end.column } var id = { major: monacoIdentifierTemplate.major, minor: monacoIdentifierTemplate.minor++ } monacoInstance.executeEdits('Yjs', [{ id: id, range: range, text: text, forceMoveMarkers: true }]) }) } this.observe(yCallback) this.monacoInstances.push({ editor: monacoInstance, yCallback: yCallback, monacoCallback: monacoCallback, disposeBinding: disposeBinding }) } // CodeMirror implementation.. unbindCodeMirror (codeMirrorInstance) { var i = this.codeMirrorInstances.findIndex(function (binding) { return binding.editor === codeMirrorInstance }) if (i >= 0) { var binding = this.codeMirrorInstances[i] this.unobserve(binding.yCallback) binding.editor.off('changes', binding.codeMirrorCallback) this.codeMirrorInstances.splice(i, 1) } } unbindCodeMirrorAll () { for (let i = this.codeMirrorInstances.length - 1; i >= 0; i--) { this.unbindCodeMirror(this.codeMirrorInstances[i].editor) } } bindCodeMirror (codeMirrorInstance, options) { var self = this options = options || {} // this function makes sure that either the // codemirror event is executed, or the yjs observer is executed var token = true function mutualExcluse (f) { if (token) { token = false try { f() } catch (e) { token = true throw new Error(e) } token = true } } codeMirrorInstance.setValue(this.toString()) function codeMirrorCallback (cm, deltas) { mutualExcluse(function () { for (var i = 0; i < deltas.length; i++) { var delta = deltas[i] var start = codeMirrorInstance.indexFromPos(delta.from) // apply the delete operation first if (delta.removed.length > 0) { var delLength = 0 for (var j = 0; j < delta.removed.length; j++) { delLength += delta.removed[j].length } // "enter" is also a character in our case delLength += delta.removed.length - 1 self.delete(start, delLength) } // apply insert operation self.insert(start, delta.text.join('\n')) } }) } codeMirrorInstance.on('changes', codeMirrorCallback) function yCallback (event) { mutualExcluse(function () { let from = codeMirrorInstance.posFromIndex(event.index) if (event.type === 'insert') { let to = from codeMirrorInstance.replaceRange(event.values.join(''), from, to) } else if (event.type === 'delete') { let to = codeMirrorInstance.posFromIndex(event.index + event.length) codeMirrorInstance.replaceRange('', from, to) } }) } this.observe(yCallback) this.codeMirrorInstances.push({ editor: codeMirrorInstance, yCallback: yCallback, codeMirrorCallback: codeMirrorCallback }) } unbindAce (aceInstance) { var i = this.aceInstances.findIndex(function (binding) { return binding.editor === aceInstance }) if (i >= 0) { var binding = this.aceInstances[i] this.unobserve(binding.yCallback) binding.editor.off('change', binding.aceCallback) this.aceInstances.splice(i, 1) } } unbindAceAll () { for (let i = this.aceInstances.length - 1; i >= 0; i--) { this.unbindAce(this.aceInstances[i].editor) } } bindAce (aceInstance, options) { var self = this options = options || {} // this function makes sure that either the // ace event is executed, or the yjs observer is executed var token = true function mutualExcluse (f) { if (token) { token = false try { f() } catch (e) { token = true throw new Error(e) } token = true } } aceInstance.setValue(this.toString()) function aceCallback (delta) { mutualExcluse(function () { var start var length var aceDocument = aceInstance.getSession().getDocument() if (delta.action === 'insert') { start = aceDocument.positionToIndex(delta.start, 0) self.insert(start, delta.lines.join('\n')) } else if (delta.action === 'remove') { start = aceDocument.positionToIndex(delta.start, 0) length = delta.lines.join('\n').length self.delete(start, length) } }) } aceInstance.on('change', aceCallback) aceInstance.selection.clearSelection() // We don't that ace is a global variable // see #2 var aceClass if (typeof ace !== 'undefined' && options.aceClass == null) { aceClass = ace // eslint-disable-line } else { aceClass = options.aceClass } var aceRequire = options.aceRequire || aceClass.require var Range = aceRequire('ace/range').Range function yCallback (event) { var aceDocument = aceInstance.getSession().getDocument() mutualExcluse(function () { if (event.type === 'insert') { let start = aceDocument.indexToPosition(event.index, 0) aceDocument.insert(start, event.values.join('')) } else if (event.type === 'delete') { let start = aceDocument.indexToPosition(event.index, 0) let end = aceDocument.indexToPosition(event.index + event.length, 0) var range = new Range(start.row, start.column, end.row, end.column) aceDocument.remove(range) } }) } this.observe(yCallback) this.aceInstances.push({ editor: aceInstance, yCallback: yCallback, aceCallback: aceCallback }) } bind () { var e = arguments[0] if (e instanceof Element) { this.bindTextarea.apply(this, arguments) } else if (e != null && e.session != null && e.getSession != null && e.setValue != null) { this.bindAce.apply(this, arguments) } else if (e != null && e.posFromIndex != null && e.replaceRange != null) { this.bindCodeMirror.apply(this, arguments) } else if (e != null && e.onDidChangeModelContent != null) { this.bindMonaco.apply(this, arguments) } else { console.error('Cannot bind, unsupported editor!') } } unbindTextarea (textarea) { var i = this.textfields.findIndex(function (binding) { return binding.editor === textarea }) if (i >= 0) { var binding = this.textfields[i] this.unobserve(binding.yCallback) var e = binding.editor e.removeEventListener('input', binding.eventListener) this.textfields.splice(i, 1) } } unbindTextareaAll () { for (let i = this.textfields.length - 1; i >= 0; i--) { this.unbindTextarea(this.textfields[i].editor) } } bindTextarea (textfield, domRoot) { domRoot = domRoot || window; // eslint-disable-line if (domRoot.getSelection == null) { domRoot = window; // eslint-disable-line } // don't duplicate! for (var t = 0; t < this.textfields.length; t++) { if (this.textfields[t].editor === textfield) { return } } // this function makes sure that either the // textfieldt event is executed, or the yjs observer is executed var token = true function mutualExcluse (f) { if (token) { token = false try { f() } catch (e) { token = true throw new Error(e) } token = true } } var self = this textfield.value = this.toString() var createRange, writeRange, writeContent, getContent if (textfield.selectionStart != null && textfield.setSelectionRange != null) { createRange = function (fix) { var left = textfield.selectionStart var right = textfield.selectionEnd if (fix != null) { left = fix(left) right = fix(right) } return { left: left, right: right } } writeRange = function (range) { writeContent(self.toString()) textfield.setSelectionRange(range.left, range.right) } writeContent = function (content) { textfield.value = content } getContent = function () { return textfield.value } } else { createRange = function (fix) { var range = {} var s = domRoot.getSelection() var clength = textfield.textContent.length range.left = Math.min(s.anchorOffset, clength) range.right = Math.min(s.focusOffset, clength) if (fix != null) { range.left = fix(range.left) range.right = fix(range.right) } var editedElement = s.focusNode if (editedElement === textfield || editedElement === textfield.childNodes[0]) { range.isReal = true } else { range.isReal = false } return range } writeRange = function (range) { writeContent(self.toString()) var textnode = textfield.childNodes[0] if (range.isReal && textnode != null) { if (range.left < 0) { range.left = 0 } range.right = Math.max(range.left, range.right) if (range.right > textnode.length) { range.right = textnode.length } range.left = Math.min(range.left, range.right) var r = document.createRange(); // eslint-disable-line r.setStart(textnode, range.left) r.setEnd(textnode, range.right) var s = domRoot.getSelection(); // eslint-disable-line s.removeAllRanges() s.addRange(r) } } writeContent = function (content) { textfield.innerText = content /* var contentArray = content.replace(new RegExp('\n', 'g'), ' ').split(' '); // eslint-disable-line textfield.innerText = '' for (var i = 0; i < contentArray.length; i++) { var c = contentArray[i] textfield.innerText += c if (i !== contentArray.length - 1) { textfield.innerHTML += ' ' } } */ } getContent = function () { return textfield.innerText } } writeContent(this.toString()) function yCallback (event) { mutualExcluse(() => { var oPos, fix if (event.type === 'insert') { oPos = event.index fix = function (cursor) { // eslint-disable-line if (cursor <= oPos) { return cursor } else { cursor += 1 return cursor } } var r = createRange(fix) writeRange(r) } else if (event.type === 'delete') { oPos = event.index fix = function (cursor) { // eslint-disable-line if (cursor < oPos) { return cursor } else { cursor -= 1 return cursor } } r = createRange(fix) writeRange(r) } }) } this.observe(yCallback) var textfieldObserver = function textfieldObserver () { mutualExcluse(function () { var r = createRange(function (x) { return x }) var oldContent = self.toString() var content = getContent() var diffs = diff(oldContent, content, r.left) var pos = 0 for (var i = 0; i < diffs.length; i++) { var d = diffs[i] if (d[0] === 0) { // EQUAL pos += d[1].length } else if (d[0] === -1) { // DELETE self.delete(pos, d[1].length) } else { // INSERT self.insert(pos, d[1]) pos += d[1].length } } }) } textfield.addEventListener('input', textfieldObserver) this.textfields.push({ editor: textfield, yCallback: yCallback, eventListener: textfieldObserver }) } _destroy () { this.unbindAll() this.textfields = null this.aceInstances = null super._destroy() } } Y.extend('Text', new Y.utils.CustomTypeDefinition({ name: 'Text', class: YText, struct: 'List', initType: function * YTextInitializer (os, model) { var _content = [] yield * Y.Struct.List.map.call(this, model, function (op) { if (op.hasOwnProperty('opContent')) { throw new Error('Text must not contain types!') } else { op.content.forEach(function (c, i) { _content.push({ id: [op.id[0], op.id[1] + i], val: op.content[i] }) }) } }) return new YText(os, model.id, _content) }, createType: function YTextCreator (os, model) { return new YText(os, model.id, []) } })) }) } module.exports = extend if (typeof Y !== 'undefined') { extend(Y) } },{"fast-diff":1}]},{},[2])(2) });