\
+ visit \
+
\
+ \
+ \
+
From 10c9811fc534a2738c19d8f74a447ed500b730a2 Mon Sep 17 00:00:00 2001
From: Wu Cheng-Han
Date: Thu, 2 Jul 2015 00:10:20 +0800
Subject: Jump to 0.3.1
---
public/js/cover.js | 130 ++++++++++--
public/js/extra.js | 293 ++++++++++++++++++++------
public/js/fb.js | 8 -
public/js/history.js | 3 +-
public/js/index.js | 541 +++++++++++++++++++++++++++++++++++++++---------
public/js/pretty.js | 84 +++++++-
public/js/syncscroll.js | 13 +-
7 files changed, 878 insertions(+), 194 deletions(-)
delete mode 100644 public/js/fb.js
(limited to 'public/js')
diff --git a/public/js/cover.js b/public/js/cover.js
index 322768bf..49fcddeb 100644
--- a/public/js/cover.js
+++ b/public/js/cover.js
@@ -4,12 +4,17 @@ var options = {
\
\
\
- \
+ visit \
+
\
-
\
+ \
+ \
+
' + value.time);
+ clearHistory = false;
+ deleteId = id;
+ });
+ buildTagsFilter(filtertags);
+}
+
+//auto update item fromNow every minutes
+setInterval(updateItemFromNow, 60000);
+
+function updateItemFromNow() {
+ var items = $('.item').toArray();
+ for (var i = 0; i < items.length; i++) {
+ var item = $(items[i]);
+ var timestamp = parseInt(item.find('.timestamp').text());
+ item.find('.fromNow').text(moment(timestamp).fromNow());
+ }
+}
+
+var clearHistory = false;
+var deleteId = null;
+
+function deleteHistory() {
+ if (clearHistory) {
+ saveHistory([]);
+ historyList.clear();
+ checkHistoryList();
+ } else {
+ if (!deleteId) return;
getHistory(function (notehistory) {
- var newnotehistory = removeHistory(id, notehistory);
+ var newnotehistory = removeHistory(deleteId, notehistory);
saveHistory(newnotehistory);
});
- list.remove('id', id);
+ historyList.remove('id', deleteId);
checkHistoryList();
- });
- buildTagsFilter(filtertags);
+ }
+ $('.delete-modal').modal('hide');
+ clearHistory = false;
+ deleteId = null;
}
+$(".ui-delete-modal-confirm").click(function () {
+ deleteHistory();
+});
+
$(".ui-import-from-browser").click(function () {
saveStorageHistoryToServer(function () {
parseStorageToHistory(historyList, parseHistoryCallback);
@@ -160,9 +202,10 @@ $(".ui-open-history").bind("change", function (e) {
});
$(".ui-clear-history").click(function () {
- saveHistory([]);
- historyList.clear();
- checkHistoryList();
+ $('.ui-delete-modal-msg').text('Do you really want to clear all history?');
+ $('.ui-delete-modal-item').html('There is no turning back.');
+ clearHistory = true;
+ deleteId = null;
});
$(".ui-refresh-history").click(function () {
@@ -229,6 +272,67 @@ var source = $("#template").html();
var template = Handlebars.compile(source);
var context = {
release: [
+ {
+ version: "0.3.1",
+ tag: "clearsky",
+ date: moment("201506301600", 'YYYYMMDDhhmm').fromNow(),
+ detail: [
+ {
+ title: "Features",
+ item: [
+ "+ Added auto table of content",
+ "+ Added basic permission control",
+ "+ Added view count in share note"
+ ]
+ },
+ {
+ title: "Enhancements",
+ item: [
+ "* Toolbar now will hide in single view",
+ "* History time now will auto update",
+ "* Smooth scroll on anchor changed",
+ "* Updated video style"
+ ]
+ },
+ {
+ title: "Fixes",
+ item: [
+ "* Note might not clear when all users disconnect",
+ "* Blockquote tag not parsed properly",
+ "* History style not correct"
+ ]
+ }
+ ]
+ },
+ {
+ version: "0.3.0",
+ tag: "sunrise",
+ date: moment("201506152400", 'YYYYMMDDhhmm').fromNow(),
+ detail: [
+ {
+ title: "Enhancements",
+ item: [
+ "* Used short url in share notes",
+ "* Added upload image button on toolbar",
+ "* Share notes are now SEO and mobile friendly",
+ "* Updated code block style",
+ "* Newline now will cause line breaks",
+ "* Image now will link out",
+ "* Used otk to avoid race condition",
+ "* Used hash to avoid data inconsistency",
+ "* Optimized server realtime script"
+ ]
+ },
+ {
+ title: "Fixes",
+ item: [
+ "* Composition input might lost or duplicated when other input involved",
+ "* Note title might not save properly",
+ "* Todo list not render properly"
+ ]
+ }
+ ]
+ },
{
version: "0.2.9",
tag: "wildfire",
diff --git a/public/js/extra.js b/public/js/extra.js
index 495c5677..f6d47647 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -1,43 +1,75 @@
+//auto update last change
+var lastchangetime = null;
+var lastchangeui = null;
+
+function updateLastChange() {
+ if (lastchangetime && lastchangeui) {
+ lastchangeui.html(' change ' + moment(lastchangetime).fromNow());
+ lastchangeui.attr('title', moment(lastchangetime).format('llll'));
+ }
+}
+setInterval(updateLastChange, 60000);
+
//get title
function getTitle(view) {
var h1s = view.find("h1");
var title = "";
- if (h1s.length > 0) {
+ if (h1s.length > 0) {
title = h1s.first().text();
} else {
title = null;
}
return title;
}
+
//render title
function renderTitle(view) {
var title = getTitle(view);
- if (title) {
+ if (title) {
title += ' - HackMD';
} else {
title = 'HackMD - Collaborative notes';
}
return title;
}
+
//render filename
function renderFilename(view) {
var filename = getTitle(view);
- if (!filename) {
+ if (!filename) {
filename = 'Untitled';
}
return filename;
}
+function slugifyWithUTF8(text) {
+ var newText = S(text.toLowerCase()).trim().stripTags().dasherize().s;
+ newText = newText.replace(/([\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '');
+ return newText;
+}
+
var viewAjaxCallback = null;
+//regex for blockquote
+var spaceregex = /\s*/;
+var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
+var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/;
+coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
+var nameregex = /\[name=(.*?)\]/;
+var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*?)\]/;
+var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
+nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
+timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
+
//dynamic event or object binding here
function finishView(view) {
//youtube
- view.find(".youtube").click(function () {
- imgPlayiframe(this, '//www.youtube.com/embed/');
- });
+ view.find(".youtube.raw").removeClass("raw")
+ .click(function () {
+ imgPlayiframe(this, '//www.youtube.com/embed/');
+ });
//vimeo
- view.find(".vimeo")
+ view.find(".vimeo.raw").removeClass("raw")
.click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/');
})
@@ -54,35 +86,32 @@ function finishView(view) {
});
});
//gist
- view.find("code[data-gist-id]").each(function(key, value) {
- if($(value).children().length == 0)
+ view.find("code[data-gist-id]").each(function (key, value) {
+ if ($(value).children().length == 0)
$(value).gist(viewAjaxCallback);
});
//emojify
emojify.run(view[0]);
//mathjax
- var mathjaxdivs = view.find('.mathjax').toArray();
+ var mathjaxdivs = view.find('.mathjax.raw').removeClass("raw").toArray();
try {
for (var i = 0; i < mathjaxdivs.length; i++) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[i].innerHTML]);
MathJax.Hub.Queue(viewAjaxCallback);
- $(mathjaxdivs[i]).removeClass("mathjax");
}
- } catch(err) {
- }
+ } catch (err) {}
//sequence diagram
- var sequence = view.find(".sequence-diagram");
+ var sequence = view.find(".sequence-diagram.raw").removeClass("raw");
try {
sequence.sequenceDiagram({
theme: 'simple'
});
sequence.parent().parent().replaceWith(sequence);
- sequence.removeClass("sequence-diagram");
- } catch(err) {
+ } catch (err) {
console.error(err);
}
//flowchart
- var flow = view.find(".flow-chart");
+ var flow = view.find(".flow-chart.raw").removeClass("raw");
flow.each(function (key, value) {
try {
var chart = flowchart.parse($(value).text());
@@ -94,26 +123,41 @@ function finishView(view) {
'font-family': "'Andale Mono', monospace"
});
$(value).parent().parent().replaceWith(value);
- $(value).removeClass("flow-chart");
- } catch(err) {
+ } catch (err) {
console.error(err);
}
});
+ //image href new window(emoji not included)
+ var images = view.find("p > img[src]:not([class])");
+ images.each(function (key, value) {
+ var src = $(value).attr('src');
+ var a = $('');
+ if (src) {
+ a.attr('href', src);
+ a.attr('target', "_blank");
+ }
+ a.html($(value).clone());
+ $(value).replaceWith(a);
+ });
+ //blockquote
+ var blockquote = view.find("blockquote.raw").removeClass("raw");
+ var blockquote_p = blockquote.find("p");
+ blockquote_p.each(function (key, value) {
+ var html = $(value).html();
+ html = html.replace(coloregex, '');
+ html = html.replace(nameandtimeregex, ' $1 $2');
+ html = html.replace(nameregex, ' $1');
+ html = html.replace(timeregex, ' $1');
+ $(value).html(html);
+ });
+ var blockquote_color = blockquote.find(".color");
+ blockquote_color.each(function (key, value) {
+ $(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
+ });
//render title
document.title = renderTitle(view);
}
-//regex for blockquote
-var spaceregex = /\s*/;
-var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
-var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*)\]/;
-coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
-var nameregex = /\[name=([-|_|\s|\w]*)\]/;
-var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*)\]/;
-var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
-nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
-timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
-
//only static transform should be here
function postProcess(code) {
var result = $('
\n'; +}; +md.renderer.rules.hardbreak = function (tokens, idx, options /*, env */ ) { + return md.options.xhtmlOut ? '
' : '
'; +}; +md.renderer.rules.fence = function (tokens, idx, options, env, self) { + var token = tokens[idx]; + var langClass = ''; + var langPrefix = options.langPrefix; + var langName = '', + fenceName; + var highlighted; + + if (token.params) { + + // + // ```foo bar + // + // Try custom renderer "foo" first. That will simplify overwrite + // for diagrams, latex, and any other fenced block with custom look + // + + fenceName = token.params.split(/\s+/g)[0]; + + if (Remarkable.utils.has(self.rules.fence_custom, fenceName)) { + return self.rules.fence_custom[fenceName](tokens, idx, options, env, self); + } + + langName = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(Remarkable.utils.unescapeMd(fenceName))); + langClass = ' class="' + langPrefix + langName.replace('=', '') + ' hljs"'; + } + + if (options.highlight) { + highlighted = options.highlight(token.content, langName) || Remarkable.utils.escapeHtml(token.content); + } else { + highlighted = Remarkable.utils.escapeHtml(token.content); + } + + return '' + md.renderer.getBreak(tokens, idx); +}; //youtube var youtubePlugin = new Plugin( // regexp to match @@ -251,7 +418,7 @@ var youtubePlugin = new Plugin( function (match, utils) { var videoid = match[1]; if (!videoid) return; - var div = $(''); + var div = $(''); setSizebyAttr(div, div); div.attr('videoid', videoid); var icon = ''; @@ -270,7 +437,7 @@ var vimeoPlugin = new Plugin( function (match, utils) { var videoid = match[1]; if (!videoid) return; - var div = $(''); + var div = $(''); setSizebyAttr(div, div); div.attr('videoid', videoid); var icon = ''; @@ -298,7 +465,7 @@ var mathjaxPlugin = new Plugin( // this function will be called when something matches function (match, utils) { //var code = $(match).text(); - return '' + match[0] + ''; + return '' + match[0] + ''; } ); md.use(youtubePlugin); diff --git a/public/js/fb.js b/public/js/fb.js deleted file mode 100644 index 0bb7a466..00000000 --- a/public/js/fb.js +++ /dev/null @@ -1,8 +0,0 @@ -(function (d, s, id) { - var js, fjs = d.getElementsByTagName(s)[0]; - if (d.getElementById(id)) return; - js = d.createElement(s); - js.id = id; - js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.3&appId=1436904003272070"; - fjs.parentNode.insertBefore(js, fjs); -}(document, 'script', 'facebook-jssdk')); \ No newline at end of file diff --git a/public/js/history.js b/public/js/history.js index c5db94c5..15e46cc6 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -319,8 +319,9 @@ function parseToHistory(list, notehistory, callback) { else if (notehistory && notehistory.length > 0) { for (var i = 0; i < notehistory.length; i++) { //parse time to timestamp and fromNow - notehistory[i].timestamp = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').unix(); + notehistory[i].timestamp = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').valueOf(); notehistory[i].fromNow = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').fromNow(); + notehistory[i].time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').format('llll'); if (list.get('id', notehistory[i].id).length == 0) list.add(notehistory[i]); } diff --git a/public/js/index.js b/public/js/index.js index 24b38f75..5f7e6992 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,16 +1,21 @@ //constant vars //settings var debug = false; -var version = '0.2.9'; +var version = '0.3.1'; var defaultTextHeight = 18; var viewportMargin = 20; var defaultExtraKeys = { + "Cmd-S": function () { + return CodeMirror.PASS + }, + "Ctrl-S": function () { + return CodeMirror.PASS + }, "Enter": "newlineAndIndentContinueMarkdownList" }; var idleTime = 300000; //5 mins -var doneTypingDelay = 400; var finishChangeDelay = 400; var cursorActivityDelay = 50; var cursorAnimatePeriod = 100; @@ -97,12 +102,27 @@ var supportExternals = [ search: 'gist' } ]; -var supportGenerals = [ +var supportBlockquoteTags = [ + { + text: '[name tag]', + search: '[]', + command: function () { + return '[name=' + personalInfo.name + ']'; + }, + }, { + text: '[time tag]', + search: '[]', command: function () { - return moment().format('llll'); + return '[time=' + moment().format('llll') + ']'; }, - search: 'time' + }, + { + text: '[color tag]', + search: '[]', + command: function () { + return '[color=' + personalInfo.color + ']'; + } } ]; var modeType = { @@ -131,6 +151,7 @@ var defaultMode = modeType.both; //global vars var loaded = false; +var needRefresh = false; var isDirty = false; var editShown = false; var visibleXS = false; @@ -192,7 +213,7 @@ var editor = CodeMirror.fromTextArea(textit, { extraKeys: defaultExtraKeys, readOnly: true }); -inlineAttachment.editors.codemirror4.attach(editor); +var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor); defaultTextHeight = parseInt($(".CodeMirror").css('line-height')); //ui vars @@ -203,7 +224,7 @@ var ui = { shortStatus: $(".ui-short-status"), status: $(".ui-status"), new: $(".ui-new"), - pretty: $(".ui-pretty"), + share: $(".ui-share"), download: { markdown: $(".ui-download-markdown") }, @@ -217,7 +238,24 @@ var ui = { mode: $(".ui-mode"), edit: $(".ui-edit"), view: $(".ui-view"), - both: $(".ui-both") + both: $(".ui-both"), + uploadImage: $(".ui-upload-image") + }, + infobar: { + lastchange: $(".ui-lastchange"), + permission: { + permission: $(".ui-permission"), + label: $(".ui-permission-label"), + freely: $(".ui-permission-freely"), + editable: $(".ui-permission-editable"), + locked: $(".ui-permission-locked") + } + }, + toc: { + toc: $('.ui-toc'), + affix: $('.ui-affix-toc'), + label: $('.ui-toc-label'), + dropdown: $('.ui-toc-dropdown') }, area: { edit: $(".ui-edit-area"), @@ -263,10 +301,16 @@ function idleStateChange() { updateOnlineStatus(); } -loginStateChangeEvent = function () { - location.reload(true); +function setNeedRefresh() { + $('#refreshModal').modal('show'); + needRefresh = true; + editor.setOption('readOnly', true); + socket.disconnect(); + showStatus(statusType.offline); } +loginStateChangeEvent = setNeedRefresh; + //visibility var wasFocus = false; Visibility.change(function (e, state) { @@ -315,6 +359,11 @@ $(document).ready(function () { $body.removeClass('fixfixed'); }); } + //showup + $().showUp('.navbar', { + upClass: 'navbar-hide', + downClass: 'navbar-show' + }); }); //when page resize var windowResizeDelay = 200; @@ -327,12 +376,38 @@ $(window).resize(function () { }); //when page unload $(window).unload(function () { - emitRefresh(); + emitUpdate(); }); +//when page hash change +window.onhashchange = locationHashChanged; + +function locationHashChanged(e) { + e.stopPropagation(); + e.preventDefault(); + if (currentMode != modeType.both) { + return; + } + var hashtarget = $("[id$='" + location.hash.substr(1) + "']"); + if (hashtarget.length > 0) { + var linenumber = hashtarget.attr('data-startline'); + if (linenumber) { + editor.setOption('viewportMargin', Infinity); + editor.setOption('viewportMargin', viewportMargin); + var t = editor.charCoords({ + line: linenumber, + ch: 0 + }, "local").top; + editor.scrollTo(null, t - defaultTextHeight * 1.2); + } + } +} + function windowResize() { checkResponsive(); checkEditorStyle(); + checkTocStyle(); + //refresh editor if (loaded) { editor.setOption('viewportMargin', Infinity); setTimeout(function () { @@ -373,6 +448,39 @@ function checkEditorStyle() { } } +function checkTocStyle() { + //toc right + var paddingRight = parseFloat(ui.area.markdown.css('padding-right')); + var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight)); + ui.toc.toc.css('right', right + 'px'); + //affix toc left + var newbool; + var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2; + //for ipad or wider device + if (rightMargin >= 133) { + newbool = true; + var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2; + var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin; + ui.toc.affix.css('left', left + 'px'); + } else { + newbool = false; + } + //toc scrollspy + ui.toc.toc.removeClass('scrollspy-body, scrollspy-view'); + ui.toc.affix.removeClass('scrollspy-body, scrollspy-view'); + if (currentMode != modeType.both && !newbool) { + ui.toc.toc.addClass('scrollspy-body'); + ui.toc.affix.addClass('scrollspy-body'); + } else { + ui.toc.toc.addClass('scrollspy-view'); + ui.toc.affix.addClass('scrollspy-body'); + } + if (newbool != enoughForAffixToc) { + enoughForAffixToc = newbool; + generateScrollspy(); + } +} + function showStatus(type, num) { currentStatus = type; var shortStatus = ui.toolbar.shortStatus; @@ -461,12 +569,21 @@ function changeMode(type) { } if (currentMode != modeType.view && visibleLG) { //editor.focus(); - editor.refresh(); + //editor.refresh(); } else { editor.getInputField().blur(); } - if (changeMode != modeType.edit) + if (currentMode == modeType.edit || currentMode == modeType.both) { + ui.toolbar.uploadImage.fadeIn(); + } else { + ui.toolbar.uploadImage.fadeOut(); + } + if (currentMode != modeType.edit) { + $(document.body).css('background-color', 'white'); updateView(); + } else { + $(document.body).css('background-color', ui.area.codemirror.css('background-color')); + } restoreInfo(); windowResize(); @@ -489,10 +606,9 @@ function changeMode(type) { } //button actions -var noteId = window.location.pathname.split('/')[1]; -var url = window.location.origin + '/' + noteId; -//pretty -ui.toolbar.pretty.attr("href", url + "/pretty"); +var url = window.location.pathname; +//share +ui.toolbar.share.attr("href", url + "/share"); //download //markdown ui.toolbar.download.markdown.click(function () { @@ -534,6 +650,73 @@ ui.toolbar.import.dropbox.click(function () { ui.toolbar.import.clipboard.click(function () { //na }); +//upload image +ui.toolbar.uploadImage.bind('change', function (e) { + var files = e.target.files || e.dataTransfer.files; + e.dataTransfer = {}; + e.dataTransfer.files = files; + inlineAttach.onDrop(e); +}); +//toc +ui.toc.dropdown.click(function (e) { + e.stopPropagation(); +}); + +function scrollToTop() { + if (currentMode == modeType.both) { + if (editor.getScrollInfo().top != 0) + editor.scrollTo(0, 0); + else + ui.area.view.animate({ + scrollTop: 0 + }, 100, "linear"); + } else { + $(document.body).animate({ + scrollTop: 0 + }, 100, "linear"); + } +} + +function scrollToBottom() { + if (currentMode == modeType.both) { + var scrollInfo = editor.getScrollInfo(); + var scrollHeight = scrollInfo.height; + if (scrollInfo.top != scrollHeight) + editor.scrollTo(0, scrollHeight * 2); + else + ui.area.view.animate({ + scrollTop: ui.area.view[0].scrollHeight + }, 100, "linear"); + } else { + $(document.body).animate({ + scrollTop: $(document.body)[0].scrollHeight + }, 100, "linear"); + } +} + +var enoughForAffixToc = true; + +//scrollspy +function generateScrollspy() { + $(document.body).scrollspy({ + target: '.scrollspy-body' + }); + ui.area.view.scrollspy({ + target: '.scrollspy-view' + }); + $(document.body).scrollspy('refresh'); + ui.area.view.scrollspy('refresh'); + if (enoughForAffixToc) { + ui.toc.toc.hide(); + ui.toc.affix.show(); + } else { + ui.toc.affix.hide(); + ui.toc.toc.show(); + } + $(document.body).scroll(); + ui.area.view.scroll(); +} + //fix for wrong autofocus $('#clipboardModal').on('shown.bs.modal', function () { $('#clipboardModal').blur(); @@ -549,6 +732,9 @@ $("#clipboardModalConfirm").click(function () { $("#clipboardModalContent").html(''); } }); +$('#refreshModalRefresh').click(function () { + location.reload(true); +}); function parseToEditor(data) { var parsed = toMarkdown(data); @@ -585,19 +771,20 @@ function importFromUrl(url) { } function isValidURL(str) { - var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator - if (!pattern.test(str)) { - return false; - } else { - return true; - } + var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + if (!pattern.test(str)) { + return false; + } else { + return true; } - //mode +} + +//mode ui.toolbar.mode.click(function () { toggleMode(); }); @@ -613,18 +800,63 @@ ui.toolbar.view.click(function () { ui.toolbar.both.click(function () { changeMode(modeType.both); }); +//permission +//freely +ui.infobar.permission.freely.click(function () { + updatePermission("freely"); +}); +//editable +ui.infobar.permission.editable.click(function () { + updatePermission("editable"); +}); +//locked +ui.infobar.permission.locked.click(function () { + updatePermission("locked"); +}); + +function updatePermission(_permission) { + if (_permission != permission) { + socket.emit('permission', _permission); + } +} + +function checkPermission() { + var label = null; + var title = null; + switch (permission) { + case "freely": + label = ' Freely'; + title = "Anyone can edit"; + break; + case "editable": + label = ' Editable'; + title = "Signed people can edit"; + break; + case "locked": + label = ' Locked'; + title = "Only owner can edit"; + break; + } + if (personalInfo.userid == owner) { + label += ' '; + ui.infobar.permission.label.removeClass('disabled'); + } else { + ui.infobar.permission.label.addClass('disabled'); + } + ui.infobar.permission.label.html(label).attr('title', title); +} //socket.io actions var socket = io.connect(); //overwrite original event for checking login state var on = socket.on; socket.on = function () { - if (!checkLoginStateChanged()) + if (!checkLoginStateChanged() && !needRefresh) on.apply(socket, arguments); }; var emit = socket.emit; socket.emit = function () { - if (!checkLoginStateChanged()) + if (!checkLoginStateChanged() && !needRefresh) emit.apply(socket, arguments); }; socket.on('info', function (data) { @@ -653,17 +885,53 @@ socket.on('connect', function (data) { }); socket.on('version', function (data) { if (data != version) - location.reload(true); + setNeedRefresh(); +}); +socket.on('check', function (data) { + if (data.id == socket.id) { + lastchangetime = data.updatetime; + lastchangeui = ui.infobar.lastchange; + updateLastChange(); + return; + } + var currentHash = md5(LZString.compressToUTF16(editor.getValue())); + var hashMismatch = (currentHash != data.hash); + if (hashMismatch) + socket.emit('refresh'); + else { + lastchangetime = data.updatetime; + lastchangeui = ui.infobar.lastchange; + updateLastChange(); + } }); +socket.on('permission', function (data) { + permission = data.permission; + checkPermission(); +}); +var otk = null; +var owner = null; +var permission = null; socket.on('refresh', function (data) { + var currentHash = md5(LZString.compressToUTF16(editor.getValue())); + var hashMismatch = (currentHash != data.hash); saveInfo(); - var body = data.body; - body = LZString.decompressFromUTF16(body); - if (body) - editor.setValue(body); - else - editor.setValue(""); + otk = data.otk; + owner = data.owner; + permission = data.permission; + + if (hashMismatch) { + var body = data.body; + body = LZString.decompressFromUTF16(body); + if (body) + editor.setValue(body); + else + editor.setValue(""); + } + + lastchangetime = data.updatetime; + lastchangeui = ui.infobar.lastchange; + updateLastChange(); if (!loaded) { editor.clearHistory(); @@ -673,9 +941,19 @@ socket.on('refresh', function (data) { loaded = true; emitUserStatus(); //send first user status updateOnlineStatus(); //update first online status + setTimeout(function () { + //work around editor not refresh + editor.refresh(); + //work around cursor not refresh + for (var i = 0; i < onlineUsers.length; i++) { + buildCursor(onlineUsers[i]); + } + //work around might not scroll to hash + scrollToHash(); + }, 1); } else { //if current doc is equal to the doc before disconnect - if (LZString.compressToUTF16(editor.getValue()) !== data.body) + if (hashMismatch) editor.clearHistory(); else { if (lastInfo.history) @@ -684,21 +962,58 @@ socket.on('refresh', function (data) { lastInfo.history = null; } - updateView(); + if (hashMismatch) + updateView(); if (editor.getOption('readOnly')) editor.setOption('readOnly', false); restoreInfo(); + checkPermission(); }); + +var changeStack = []; +var changeBusy = false; + socket.on('change', function (data) { data = LZString.decompressFromUTF16(data); data = JSON.parse(data); - editor.replaceRange(data.text, data.from, data.to, "ignoreHistory"); - isDirty = true; - clearTimeout(finishChangeTimer); - finishChangeTimer = setTimeout(finishChange, finishChangeDelay); + changeStack.push(data); + if (!changeBusy) + executeChange(); }); + +function executeChange() { + if (changeStack.length > 0) { + changeBusy = true; + var data = changeStack.shift(); + if (data.otk != otk) { + var found = false; + for (var i = 0, l = changeStack.length; i < l; i++) { + if (changeStack[i].otk == otk) { + changeStack.unshift(data); + data = changeStack[i]; + found = true; + break; + } + } + if (!found) { + socket.emit('refresh'); + changeBusy = false; + return; + } + } + otk = data.nextotk; + if (data.id == personalInfo.id) + editor.replaceRange(data.text, data.from, data.to, 'self::' + data.origin); + else + editor.replaceRange(data.text, data.from, data.to, "ignoreHistory"); + executeChange(); + } else { + changeBusy = false; + } +} + socket.on('online users', function (data) { data = LZString.decompressFromUTF16(data); data = JSON.parse(data); @@ -795,7 +1110,7 @@ var onlineUserList = new List('online-user-list', options); var shortOnlineUserList = new List('short-online-user-list', options); function updateOnlineStatus() { - if (!loaded) return; + if (!loaded || !socket.connected) return; var _onlineUsers = deduplicateOnlineUsers(onlineUsers); showStatus(statusType.online, _onlineUsers.length); var items = onlineUserList.items; @@ -851,7 +1166,7 @@ function sortOnlineUserList(list) { var userbIsSelf = (userb.id == personalInfo.id || (userb.login && userb.userid == personalInfo.userid)); if (useraIsSelf && !userbIsSelf) { return -1; - } else if(!useraIsSelf && userbIsSelf) { + } else if (!useraIsSelf && userbIsSelf) { return 1; } else { if (usera.login && !userb.login) @@ -975,7 +1290,7 @@ function checkCursorTag(coord, ele) { var offsetTop = defaultTextHeight; if (width > 0 && height > 0) { if (left + width + offsetLeft > editorWidth - curosrtagMargin) { - offsetLeft = -(width + 4); + offsetLeft = -(width + 10); } if (top + height + offsetTop > Math.max(viewportHeight, editorHeight) + curosrtagMargin && top - height > curosrtagMargin) { offsetTop = -(height); @@ -1124,16 +1439,61 @@ function buildCursor(user) { editor.on('beforeChange', function (cm, change) { if (debug) console.debug(change); + var self = change.origin.split('self::'); + if (self.length == 2) { + change.origin = self[1]; + self = true; + } else { + self = false; + } + if (self) { + change.canceled = true; + } else { + var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1); + if (!isIgnoreEmitEvent) { + switch (permission) { + case "freely": + //na + break; + case "editable": + if (!personalInfo.login) { + change.canceled = true; + $('.signin-modal').modal('show'); + } + break; + case "locked": + if (personalInfo.userid != owner) { + change.canceled = true; + $('.locked-modal').modal('show'); + } + break; + } + } + } }); + +var ignoreEmitEvents = ['setValue', 'ignoreHistory']; editor.on('change', function (i, op) { if (debug) console.debug(op); - if (op.origin != 'setValue' && op.origin != 'ignoreHistory') { - socket.emit('change', LZString.compressToUTF16(JSON.stringify(op))); + var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(op.origin) != -1); + if (!isIgnoreEmitEvent) { + var out = { + text: op.text, + from: op.from, + to: op.to, + origin: op.origin + }; + socket.emit('change', LZString.compressToUTF16(JSON.stringify(out))); } isDirty = true; - clearTimeout(doneTypingTimer); - doneTypingTimer = setTimeout(doneTyping, doneTypingDelay); + clearTimeout(finishChangeTimer); + finishChangeTimer = setTimeout(function () { + if (!isIgnoreEmitEvent) + finishChange(true); + else + finishChange(false); + }, finishChangeDelay); }); editor.on('focus', function (cm) { for (var i = 0; i < onlineUsers.length; i++) { @@ -1236,22 +1596,17 @@ function restoreInfo() { } //view actions -var doneTypingTimer = null; var finishChangeTimer = null; -var input = editor.getInputField(); -//user is "finished typing," do something -function doneTyping() { - emitRefresh(); - updateView(); -} -function finishChange() { +function finishChange(emit) { + if (emit) + emitUpdate(); updateView(); } -function emitRefresh() { +function emitUpdate() { var value = editor.getValue(); - socket.emit('refresh', LZString.compressToUTF16(value)); + socket.emit('update', LZString.compressToUTF16(value)); } var lastResult = null; @@ -1267,6 +1622,11 @@ function updateView() { updateDataAttrs(result, ui.area.markdown.children().toArray()); lastResult = $(result).clone(); finishView(ui.area.view); + autoLinkify(ui.area.view); + generateToc('toc'); + generateToc('toc-affix'); + generateScrollspy(); + smoothHashScroll(); writeHistory(ui.area.markdown); isDirty = false; clearMap(); @@ -1535,7 +1895,7 @@ $(editor.getInputField()) return '$1```' + lang + '=\n\n```'; }, done: function () { - editor.doc.cm.moveV(-1, "line"); + editor.doc.cm.execCommand("goLineUp"); }, context: function () { return isInCode; @@ -1556,12 +1916,19 @@ $(editor.getInputField()) return !isInCode; } }, - { //referral - match: /(^|\n|\s)(\!|\!|\[\])(\w*)$/, + { //blockquote personal info & general info + match: /(?:^|\n|\s)(\>.*)(\[\])(\w*)$/, search: function (term, callback) { - callback($.map(supportReferrals, function (referral) { - return referral.search.indexOf(term) === 0 ? referral.text : null; - })); + var list = []; + $.map(supportBlockquoteTags, function (blockquotetag) { + if (blockquotetag.search.indexOf(term) === 0) + list.push(blockquotetag.command()); + }); + $.map(supportReferrals, function (referral) { + if (referral.search.indexOf(term) === 0) + list.push(referral.text); + }) + callback(list); checkCursorMenu(); }, replace: function (value) { @@ -1571,11 +1938,11 @@ $(editor.getInputField()) return !isInCode; } }, - { //externals - match: /(^|\n|\s)\{\}(\w*)$/, + { //referral + match: /(^|\n|\s)(\!|\!|\[\])(\w*)$/, search: function (term, callback) { - callback($.map(supportExternals, function (external) { - return external.search.indexOf(term) === 0 ? external.text : null; + callback($.map(supportReferrals, function (referral) { + return referral.search.indexOf(term) === 0 ? referral.text : null; })); checkCursorMenu(); }, @@ -1586,36 +1953,16 @@ $(editor.getInputField()) return !isInCode; } }, - { //blockquote personal info & general info - match: /(^|\n|\s|\>.*)\[(\w*)=$/, - search: function (term, callback) { - var list = typeof personalInfo[term] != 'undefined' ? [personalInfo[term]] : []; - $.map(supportGenerals, function (general) { - if (general.search.indexOf(term) === 0) - list.push(general.command()); - }); - callback(list); - checkCursorMenu(); - }, - replace: function (value) { - return '$1[$2=' + value; - }, - context: function (text) { - return !isInCode; - } - }, - { //blockquote quick start tag - match: /(^.*(?!>)\n|)(\>\s{0,1})$/, + { //externals + match: /(^|\n|\s)\{\}(\w*)$/, search: function (term, callback) { - var self = '[name=' + personalInfo.name + '] [time=' + moment().format('llll') + '] [color=' + personalInfo.color + ']'; - callback([self]); + callback($.map(supportExternals, function (external) { + return external.search.indexOf(term) === 0 ? external.text : null; + })); checkCursorMenu(); }, - template: function (value) { - return '[Your name, time, color tags]'; - }, replace: function (value) { - return '$1$2' + value; + return '$1' + value; }, context: function (text) { return !isInCode; diff --git a/public/js/pretty.js b/public/js/pretty.js index 33b97803..6fff4d03 100644 --- a/public/js/pretty.js +++ b/public/js/pretty.js @@ -1,9 +1,81 @@ -var raw = $(".markdown-body").text(); -var markdown = LZString.decompressFromBase64(raw); -var result = postProcess(md.render(markdown)); var markdown = $(".markdown-body"); -markdown.html(result); -markdown.show(); +var text = $('').html(markdown.html()).text(); +var result = postProcess(md.render(text)); +markdown.html(result.html()); +$(document.body).show(); finishView(markdown); autoLinkify(markdown); -scrollToHash(); \ No newline at end of file +generateToc('toc'); +generateToc('toc-affix'); +smoothHashScroll(); +lastchangetime = $('.ui-lastchange').text(); +lastchangeui = $('.ui-lastchange'); +updateLastChange(); +var url = window.location.pathname; +$('.ui-edit').attr('href', url + '/edit'); +var toc = $('.ui-toc'); +var tocAffix = $('.ui-affix-toc'); +var tocDropdown = $('.ui-toc-dropdown'); +//toc +tocDropdown.click(function (e) { + e.stopPropagation(); +}); + +var enoughForAffixToc = true; + +function generateScrollspy() { + $(document.body).scrollspy({ + target: '' + }); + $(document.body).scrollspy('refresh'); + if (enoughForAffixToc) { + toc.hide(); + tocAffix.show(); + } else { + tocAffix.hide(); + toc.show(); + } + $(document.body).scroll(); +} + +function windowResize() { + //toc right + var paddingRight = parseFloat(markdown.css('padding-right')); + var right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight)); + toc.css('right', right + 'px'); + //affix toc left + var newbool; + var rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2; + //for ipad or wider device + if (rightMargin >= 133) { + newbool = true; + var affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2; + var left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin; + tocAffix.css('left', left + 'px'); + } else { + newbool = false; + } + if (newbool != enoughForAffixToc) { + enoughForAffixToc = newbool; + generateScrollspy(); + } +} +$(window).resize(function () { + windowResize(); +}); +$(document).ready(function () { + windowResize(); + generateScrollspy(); +}); + +function scrollToTop() { + $(document.body).animate({ + scrollTop: 0 + }, 100, "linear"); +} + +function scrollToBottom() { + $(document.body).animate({ + scrollTop: $(document.body)[0].scrollHeight + }, 100, "linear"); +} \ No newline at end of file diff --git a/public/js/syncscroll.js b/public/js/syncscroll.js index 4dee0996..48829a4a 100644 --- a/public/js/syncscroll.js +++ b/public/js/syncscroll.js @@ -8,7 +8,7 @@ md.renderer.rules.blockquote_open = function (tokens, idx /*, options, env */ ) if (tokens[idx].lines && tokens[idx].level === 0) { var startline = tokens[idx].lines[0] + 1; var endline = tokens[idx].lines[1]; - return '' + highlighted + '
\n'; + return '\n'; } return '\n'; }; @@ -55,9 +55,9 @@ md.renderer.rules.paragraph_open = function (tokens, idx) { if (tokens[idx].lines && tokens[idx].level === 0) { var startline = tokens[idx].lines[0] + 1; var endline = tokens[idx].lines[1]; - return ''; + return tokens[idx].tight ? '' : '
'; } - return ''; + return tokens[idx].tight ? '' : '
'; }; md.renderer.rules.heading_open = function (tokens, idx) { @@ -106,7 +106,7 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) { } langName = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(Remarkable.utils.unescapeMd(fenceName))); - langClass = ' class="' + langPrefix + langName + '"'; + langClass = ' class="' + langPrefix + langName.replace('=', '') + ' hljs"'; } if (options.highlight) { @@ -193,7 +193,8 @@ function buildMapInner(syncBack) { 'line-height': textarea.css('line-height'), 'word-wrap': wrap.css('word-wrap'), 'white-space': wrap.css('white-space'), - 'word-break': wrap.css('word-break') + 'word-break': wrap.css('word-break'), + 'tab-size': '38px' }).appendTo('body'); offset = ui.area.view.scrollTop() - ui.area.view.offset().top; @@ -313,7 +314,7 @@ function syncScrollToView(event, _lineNo) { var textHeight = editor.defaultTextHeight(); lineNo = Math.floor(scrollInfo.top / textHeight); //if reach bottom, then scroll to end - if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) { + if (scrollInfo.height > scrollInfo.clientHeight && scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) { posTo = ui.area.view[0].scrollHeight - ui.area.view.height(); } else { topDiffPercent = (scrollInfo.top % textHeight) / textHeight; -- cgit v1.2.3