mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-12-17 19:12:37 -05:00
Synced editor scrolling PoC.
This commit is contained in:
parent
7baa8ea0af
commit
297c25ca1f
6 changed files with 195 additions and 47 deletions
|
@ -411,7 +411,7 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
raw: this.get('reply'),
|
raw: this.get('reply'),
|
||||||
editReason: opts.editReason,
|
editReason: opts.editReason,
|
||||||
imageSizes: opts.imageSizes,
|
imageSizes: opts.imageSizes,
|
||||||
cooked: $('#wmd-preview').html()
|
cooked: this.getCookedHtml()
|
||||||
});
|
});
|
||||||
this.set('composeState', CLOSED);
|
this.set('composeState', CLOSED);
|
||||||
|
|
||||||
|
@ -448,7 +448,7 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
topic_id: this.get('topic.id'),
|
topic_id: this.get('topic.id'),
|
||||||
reply_to_post_number: post ? post.get('post_number') : null,
|
reply_to_post_number: post ? post.get('post_number') : null,
|
||||||
imageSizes: opts.imageSizes,
|
imageSizes: opts.imageSizes,
|
||||||
cooked: $('#wmd-preview').html(),
|
cooked: this.getCookedHtml(),
|
||||||
reply_count: 0,
|
reply_count: 0,
|
||||||
display_username: currentUser.get('name'),
|
display_username: currentUser.get('name'),
|
||||||
username: currentUser.get('username'),
|
username: currentUser.get('username'),
|
||||||
|
@ -534,6 +534,10 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCookedHtml: function() {
|
||||||
|
return $('#wmd-preview').html().replace(/<span class="marker"><\/span>/g, '');
|
||||||
|
},
|
||||||
|
|
||||||
saveDraft: function() {
|
saveDraft: function() {
|
||||||
// Do not save when drafts are disabled
|
// Do not save when drafts are disabled
|
||||||
if (this.get('disableDrafts')) return;
|
if (this.get('disableDrafts')) return;
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<div class='wmd-controls'>
|
<div class='wmd-controls'>
|
||||||
<div class='textarea-wrapper'>
|
<div class='textarea-wrapper'>
|
||||||
<div class='wmd-button-bar' id='wmd-button-bar'></div>
|
<div class='wmd-button-bar' id='wmd-button-bar'></div>
|
||||||
|
<div id='wmd-preview-scroller'></div>
|
||||||
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="model.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
|
{{view Discourse.NotifyingTextArea parentBinding="view" tabindex="3" valueBinding="model.reply" id="wmd-input" placeholderKey="composer.reply_placeholder"}}
|
||||||
{{popupInputTip validation=view.replyValidation shownAt=view.showReplyTip}}
|
{{popupInputTip validation=view.replyValidation shownAt=view.showReplyTip}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,14 +52,6 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, {
|
||||||
refreshPreview: Discourse.debounce(function() {
|
refreshPreview: Discourse.debounce(function() {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
this.editor.refreshPreview();
|
this.editor.refreshPreview();
|
||||||
// if the caret is on the last line ensure preview scrolled to bottom
|
|
||||||
var caretPosition = Discourse.Utilities.caretPosition(this.wmdInput[0]);
|
|
||||||
if (!this.wmdInput.val().substring(caretPosition).match(/\n/)) {
|
|
||||||
var $wmdPreview = $('#wmd-preview');
|
|
||||||
if ($wmdPreview.is(':visible')) {
|
|
||||||
$wmdPreview.scrollTop($wmdPreview[0].scrollHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 30),
|
}, 30),
|
||||||
|
|
||||||
|
|
|
@ -474,7 +474,7 @@ div.ac-wrap {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wmd-input, #wmd-preview {
|
#wmd-input, #wmd-preview-scroller, #wmd-preview {
|
||||||
@include box-sizing(border-box);
|
@include box-sizing(border-box);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -488,8 +488,11 @@ div.ac-wrap {
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 30px 0 10px;
|
margin: 30px 0 10px;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 19px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#wmd-input {
|
#wmd-input, #wmd-preview-scroller {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -502,6 +505,18 @@ div.ac-wrap {
|
||||||
@include border-radius-all(0);
|
@include border-radius-all(0);
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
#wmd-preview-scroller {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
overflow: scroll;
|
||||||
|
visibility: hidden;
|
||||||
|
.marker, .caret {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
.textarea-wrapper, .preview-wrapper {
|
.textarea-wrapper, .preview-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
@include box-sizing(border-box);
|
@include box-sizing(border-box);
|
||||||
|
|
|
@ -344,7 +344,6 @@ div.ac-wrap {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#reply-control .wmd-controls #wmd-input {font-size: 16px;}
|
|
||||||
|
|
||||||
#reply-control.edit-title.private-message {
|
#reply-control.edit-title.private-message {
|
||||||
.wmd-controls {
|
.wmd-controls {
|
||||||
|
@ -387,7 +386,7 @@ div.ac-wrap {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wmd-input, #wmd-preview {
|
#wmd-input, #wmd-preview-scroller, #wmd-preview {
|
||||||
@include box-sizing(border-box);
|
@include box-sizing(border-box);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -401,8 +400,11 @@ div.ac-wrap {
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 30px 0 10px;
|
margin: 30px 0 10px;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 19px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#wmd-input {
|
#wmd-input, #wmd-preview-scroller {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -413,6 +415,17 @@ div.ac-wrap {
|
||||||
border-top: 36px solid transparent;
|
border-top: 36px solid transparent;
|
||||||
@include border-radius-all(0);
|
@include border-radius-all(0);
|
||||||
transition: none;
|
transition: none;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#wmd-preview-scroller {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
overflow: scroll;
|
||||||
|
visibility: hidden;
|
||||||
|
.marker, .caret {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.textarea-wrapper, .preview-wrapper {
|
.textarea-wrapper, .preview-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
185
vendor/assets/javascripts/Markdown.Editor.js
vendored
185
vendor/assets/javascripts/Markdown.Editor.js
vendored
|
@ -330,6 +330,7 @@
|
||||||
function PanelCollection(postfix) {
|
function PanelCollection(postfix) {
|
||||||
this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
|
this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
|
||||||
this.preview = doc.getElementById("wmd-preview" + postfix);
|
this.preview = doc.getElementById("wmd-preview" + postfix);
|
||||||
|
this.previewScroller = doc.getElementById("wmd-preview-scroller" + postfix);
|
||||||
this.input = doc.getElementById("wmd-input" + postfix);
|
this.input = doc.getElementById("wmd-input" + postfix);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -861,6 +862,113 @@
|
||||||
var maxDelay = 3000;
|
var maxDelay = 3000;
|
||||||
var startType = "delayed"; // The other legal value is "manual"
|
var startType = "delayed"; // The other legal value is "manual"
|
||||||
|
|
||||||
|
var paneContentHeight = function(pane) {
|
||||||
|
var $pane = $(pane);
|
||||||
|
var paneVerticalPadding = parseInt($pane.css("padding-top")) + parseInt($pane.css("padding-bottom"));
|
||||||
|
|
||||||
|
return pane.scrollHeight - paneVerticalPadding;
|
||||||
|
};
|
||||||
|
|
||||||
|
var prevScrollPosition = $(panels.input).scrollTop();
|
||||||
|
var caretMarkerPosition = 0;
|
||||||
|
var markerPositions = {
|
||||||
|
scroller: [0, paneContentHeight(panels.previewScroller)],
|
||||||
|
preview: [0, paneContentHeight(panels.preview)]
|
||||||
|
};
|
||||||
|
|
||||||
|
var getCaretPosition = function() {
|
||||||
|
return Discourse.Utilities.caretPosition(panels.input);
|
||||||
|
};
|
||||||
|
|
||||||
|
var cacheCaretMarkerPosition = function() {
|
||||||
|
caretMarkerPosition = $(panels.previewScroller).find(".caret").position().top;
|
||||||
|
};
|
||||||
|
|
||||||
|
var cachePaneMarkerPositions = function(cacheName, pane) {
|
||||||
|
var $pane = $(pane);
|
||||||
|
var paneScrollPosition = $pane.scrollTop();
|
||||||
|
var panePaddingTop = parseInt($pane.css("padding-top"));
|
||||||
|
|
||||||
|
markerPositions[cacheName] = [0];
|
||||||
|
$(pane).find(".marker").each(function () {
|
||||||
|
var markerPosition = $(this).position().top + paneScrollPosition - panePaddingTop;
|
||||||
|
markerPositions[cacheName].push(markerPosition);
|
||||||
|
});
|
||||||
|
markerPositions[cacheName].push(paneContentHeight(pane));
|
||||||
|
};
|
||||||
|
|
||||||
|
var cacheMarkerPositions = function() {
|
||||||
|
cachePaneMarkerPositions("scroller", panels.previewScroller);
|
||||||
|
cachePaneMarkerPositions("preview", panels.preview);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMarkerPositions = function(syncPosition) {
|
||||||
|
var startMarkerIndex = 0;
|
||||||
|
var endMarkerIndex = markerPositions.scroller.length - 1;
|
||||||
|
|
||||||
|
for (var index = startMarkerIndex + 1; index < endMarkerIndex; index += 1) {
|
||||||
|
if (markerPositions.scroller[index] > syncPosition) {
|
||||||
|
endMarkerIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startMarkerIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollerStart: markerPositions.scroller[startMarkerIndex],
|
||||||
|
scrollerEnd: markerPositions.scroller[endMarkerIndex],
|
||||||
|
previewStart: markerPositions.preview[startMarkerIndex],
|
||||||
|
previewEnd: markerPositions.preview[endMarkerIndex]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var detectScrollDown = function(currentPosition, previousPosition) {
|
||||||
|
return (currentPosition - previousPosition >= 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getRatio = function(positions) {
|
||||||
|
return (positions.previewEnd - positions.previewStart) / (positions.scrollerEnd - positions.scrollerStart);
|
||||||
|
};
|
||||||
|
|
||||||
|
var syncScroll = function(isEdit) {
|
||||||
|
var scrollPosition = $(panels.input).scrollTop();
|
||||||
|
var isScrollDown = (scrollPosition - prevScrollPosition >= 0);
|
||||||
|
prevScrollPosition = scrollPosition;
|
||||||
|
|
||||||
|
var inputBaseline;
|
||||||
|
var previewBaseline;
|
||||||
|
var threshold;
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
inputBaseline = caretMarkerPosition;
|
||||||
|
previewBaseline = ($(panels.preview).height() * (caretMarkerPosition - scrollPosition) / $(panels.input).height());
|
||||||
|
threshold = 20;
|
||||||
|
} else if (isScrollDown) {
|
||||||
|
inputBaseline = scrollPosition + $(panels.input).height();
|
||||||
|
previewBaseline = $(panels.preview).height();
|
||||||
|
threshold = 0;
|
||||||
|
} else {
|
||||||
|
inputBaseline = scrollPosition;
|
||||||
|
previewBaseline = 0;
|
||||||
|
threshold = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var positions = getMarkerPositions(inputBaseline);
|
||||||
|
var ratio = getRatio(positions);
|
||||||
|
|
||||||
|
var newPreviewScrollPosition = positions.previewStart - previewBaseline + (inputBaseline - positions.scrollerStart) * ratio;
|
||||||
|
|
||||||
|
if (threshold == 0 || Math.abs(newPreviewScrollPosition - $(panels.preview).scrollTop()) >= threshold) {
|
||||||
|
$(panels.preview).scrollTop(newPreviewScrollPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var setupScrollSync = function() {
|
||||||
|
$(panels.input).scroll(function() {
|
||||||
|
Ember.run.throttle(null, syncScroll, 16);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Adds event listeners to elements
|
// Adds event listeners to elements
|
||||||
var setupEvents = function (inputElem, listener) {
|
var setupEvents = function (inputElem, listener) {
|
||||||
|
|
||||||
|
@ -909,14 +1017,33 @@
|
||||||
|
|
||||||
var prevTime = new Date().getTime();
|
var prevTime = new Date().getTime();
|
||||||
|
|
||||||
text = converter.makeHtml(text);
|
var caretPosition = getCaretPosition();
|
||||||
|
text = text.slice(0, caretPosition) + '~~caret~~' + text.slice(caretPosition);
|
||||||
|
text = text.replace(/(\n|\r|\r\n)(\n|\r|\r\n)+/g, "$&~~marker~~$1$1");
|
||||||
|
|
||||||
|
previewText = converter.makeHtml(text.replace('~~caret~~', ''))
|
||||||
|
.replace(/<p>~~marker~~<\/p>/g, '<span class="marker"></span>')
|
||||||
|
.replace(/~~marker~~/g, '<span class="marker"></span>');
|
||||||
|
|
||||||
|
previewScrollerText = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/(\n|\r|\r\n)/g, '<br>')
|
||||||
|
.replace('~~caret~~', '<span class="caret"></span>')
|
||||||
|
.replace(/~~marker~~<br><br>/g, '<span class="marker"></span>');
|
||||||
|
|
||||||
// Calculate the processing time of the HTML creation.
|
// Calculate the processing time of the HTML creation.
|
||||||
// It's used as the delay time in the event listener.
|
// It's used as the delay time in the event listener.
|
||||||
var currTime = new Date().getTime();
|
var currTime = new Date().getTime();
|
||||||
elapsedTime = currTime - prevTime;
|
elapsedTime = currTime - prevTime;
|
||||||
|
|
||||||
pushPreviewHtml(text);
|
Ember.run(function() {
|
||||||
|
pushPreviewHtml(previewText, previewScrollerText);
|
||||||
|
cacheMarkerPositions();
|
||||||
|
cacheCaretMarkerPosition();
|
||||||
|
syncScroll(true);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// makePreviewHtml = window.probes.measure(makePreviewHtml, {
|
// makePreviewHtml = window.probes.measure(makePreviewHtml, {
|
||||||
|
@ -990,12 +1117,6 @@
|
||||||
return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
|
return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
|
||||||
};
|
};
|
||||||
|
|
||||||
var setPanelScrollTops = function () {
|
|
||||||
if (panels.preview) {
|
|
||||||
panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.refresh = function (requiresRefresh) {
|
this.refresh = function (requiresRefresh) {
|
||||||
if (requiresRefresh) {
|
if (requiresRefresh) {
|
||||||
oldInputText = "";
|
oldInputText = "";
|
||||||
|
@ -1015,49 +1136,52 @@
|
||||||
// IE doesn't let you use innerHTML if the element is contained somewhere in a table
|
// IE doesn't let you use innerHTML if the element is contained somewhere in a table
|
||||||
// (which is the case for inline editing) -- in that case, detach the element, set the
|
// (which is the case for inline editing) -- in that case, detach the element, set the
|
||||||
// value, and reattach. Yes, that *is* ridiculous.
|
// value, and reattach. Yes, that *is* ridiculous.
|
||||||
var ieSafePreviewSet = function (text) {
|
var ieSafePreviewSet = function (previewText, previewScrollerText) {
|
||||||
var preview = panels.preview;
|
var ieSafeSet = function(panel, text) {
|
||||||
var parent = preview.parentNode;
|
var parent = panel.parentNode;
|
||||||
var sibling = preview.nextSibling;
|
var sibling = panel.nextSibling;
|
||||||
parent.removeChild(preview);
|
parent.removeChild(panel);
|
||||||
preview.innerHTML = text;
|
panel.innerHTML = text;
|
||||||
if (!sibling)
|
if (!sibling)
|
||||||
parent.appendChild(preview);
|
parent.appendChild(panel);
|
||||||
else
|
else
|
||||||
parent.insertBefore(preview, sibling);
|
parent.insertBefore(panel, sibling);
|
||||||
|
};
|
||||||
|
|
||||||
|
ieSafeSet(panels.preview, previewText);
|
||||||
|
ieSafeSet(panels.previewScroller, previewScrollerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonSuckyBrowserPreviewSet = function (text) {
|
var nonSuckyBrowserPreviewSet = function (previewText, previewScrollerText) {
|
||||||
panels.preview.innerHTML = text;
|
panels.preview.innerHTML = previewText;
|
||||||
|
panels.previewScroller.innerHTML = previewScrollerText;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewSetter;
|
var previewSetter;
|
||||||
|
|
||||||
var previewSet = function (text) {
|
var previewSet = function (previewText, previewScrollerText) {
|
||||||
|
|
||||||
if (previewSetter)
|
if (previewSetter)
|
||||||
return previewSetter(text);
|
return previewSetter(previewText, previewScrollerText);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
nonSuckyBrowserPreviewSet(text);
|
nonSuckyBrowserPreviewSet(previewText, previewScrollerText);
|
||||||
previewSetter = nonSuckyBrowserPreviewSet;
|
previewSetter = nonSuckyBrowserPreviewSet;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
previewSetter = ieSafePreviewSet;
|
previewSetter = ieSafePreviewSet;
|
||||||
previewSetter(text);
|
previewSetter(previewText, previewScrollerText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var pushPreviewHtml = function (text) {
|
var pushPreviewHtml = function (previewText, previewScrollerText) {
|
||||||
|
|
||||||
var emptyTop = position.getTop(panels.input) - getDocScrollTop();
|
var emptyTop = position.getTop(panels.input) - getDocScrollTop();
|
||||||
|
|
||||||
if (panels.preview) {
|
if (panels.preview) {
|
||||||
previewSet(text);
|
previewSet(previewText, previewScrollerText);
|
||||||
previewRefreshCallback();
|
previewRefreshCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
setPanelScrollTops();
|
|
||||||
|
|
||||||
if (isFirstTimeFilled) {
|
if (isFirstTimeFilled) {
|
||||||
isFirstTimeFilled = false;
|
isFirstTimeFilled = false;
|
||||||
return;
|
return;
|
||||||
|
@ -1080,11 +1204,10 @@
|
||||||
// TODO: make option to disable. We don't need this in discourse
|
// TODO: make option to disable. We don't need this in discourse
|
||||||
// setupEvents(panels.input, applyTimeout);
|
// setupEvents(panels.input, applyTimeout);
|
||||||
|
|
||||||
|
setupScrollSync();
|
||||||
|
|
||||||
makePreviewHtml();
|
makePreviewHtml();
|
||||||
|
|
||||||
if (panels.preview) {
|
|
||||||
panels.preview.scrollTop = 0;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
Loading…
Reference in a new issue