This commit is contained in:
Raccoon 2021-07-31 15:56:50 +08:00
parent e4855cdb3c
commit f83b4b77fd
No known key found for this signature in database
GPG Key ID: 06770355DC9ECD38
13 changed files with 672 additions and 441 deletions

58
package-lock.json generated
View File

@ -3973,6 +3973,21 @@
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz",
"integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ=="
},
"@types/jquery": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz",
"integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
@ -4014,6 +4029,12 @@
"@types/node": "*"
}
},
"@types/list.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/list.js/-/list.js-2.3.1.tgz",
"integrity": "sha512-yKwk95g7K5mKl9i5/rrkrbwoXvRtiC/gd4b1qU8mbfxanSkPsCSqAvmgaXS+tdj66+l5BMvdargh57pdOH+yBg==",
"dev": true
},
"@types/lodash": {
"version": "4.14.170",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz",
@ -4118,6 +4139,15 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/select2": {
"version": "4.0.54",
"resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.54.tgz",
"integrity": "sha512-xq8c3zsKktym2b9XbDLUWBDyX0WyWjKzeFsDQHkM9p6NfxJza4g2Ud1bheTKNXpHNyVPUrfGwDEMN7JBe3gQbw==",
"dev": true,
"requires": {
"@types/jquery": "*"
}
},
"@types/serve-static": {
"version": "1.13.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz",
@ -4127,6 +4157,12 @@
"@types/node": "*"
}
},
"@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"@types/socket.io": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.13.tgz",
@ -4147,6 +4183,12 @@
"socket.io-parser": "*"
}
},
"@types/store": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/store/-/store-2.0.2.tgz",
"integrity": "sha512-ZPHnXkzmGMfk+pHqAGzTSpA9CbsHmJLgkvOl5w52LZ0XTxB1ZIHWZzQ7lEtjTNWScBbsQekg8TjApMXkMe4nkw==",
"dev": true
},
"@types/validator": {
"version": "13.1.4",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.1.4.tgz",
@ -19059,13 +19101,21 @@
"integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM="
},
"xss": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.6.tgz",
"integrity": "sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.9.tgz",
"integrity": "sha512-2t7FahYnGJys6DpHLhajusId7R0Pm2yTmuL0GV9+mV0ZlaLSnb2toBmppATfg5sWIhZQGlsTLoecSzya+l4EAQ==",
"dev": true,
"requires": {
"commander": "^2.9.0",
"commander": "^2.20.3",
"cssfilter": "0.0.10"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
},
"xtend": {

View File

@ -119,6 +119,9 @@
"@types/express": "4.17.9",
"@types/express-flash": "0.0.2",
"@types/express-session": "^1.17.3",
"@types/jquery": "^3.5.6",
"@types/js-cookie": "^2.2.7",
"@types/list.js": "^2.3.1",
"@types/lodash": "^4.14.170",
"@types/markdown-pdf": "^9.0.0",
"@types/mime-types": "^2.1.0",
@ -128,7 +131,9 @@
"@types/passport.socketio": "^3.7.5",
"@types/qs": "^6.9.6",
"@types/randomcolor": "^0.5.5",
"@types/select2": "^4.0.54",
"@types/socket.io": "^2.1.13",
"@types/store": "^2.0.2",
"@types/validator": "^13.1.4",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
@ -222,7 +227,7 @@
"webpack-cli": "~4.7.2",
"webpack-merge": "~5.8.0",
"wurl": "~2.5.3",
"xss": "~1.0.6"
"xss": "~1.0.9"
},
"optionalDependencies": {
"bufferutil": "~4.0.0",

View File

@ -1,38 +1,115 @@
/* eslint-env browser, jquery */
/* global moment, serverurl */
import {saveAs} from 'file-saver'
import $ from 'jquery'
import List from 'list.js'
import unescapeHTML from 'lodash/unescape'
import moment from 'moment'
import {
checkIfAuth,
clearLoginState,
getLoginState,
resetCheckAuth,
setloginStateChangeEvent
} from './lib/common/login'
import '../css/cover.css'
import '../css/site.css'
import {
clearDuplicatedHistory,
deleteServerHistory,
clearServerHistoryAsync,
deleteServerHistoryAsync,
getHistory,
getHistoryAsync,
getStorageHistory,
NoteHistory,
parseHistory,
parseServerToHistory,
parseStorageToHistory,
postHistoryToServer,
postHistoryToServerAsync,
removeHistory,
saveHistory,
saveStorageHistoryToServer
} from './history'
import { saveAs } from 'file-saver'
import List from 'list.js'
import unescapeHTML from 'lodash/unescape'
import {
checkIfAuth,
clearLoginState,
getLoginState,
getLoginUserProfile, isLogin,
resetCheckAuth,
setLoginStateChangeEvent
} from './lib/common/login'
import {serverurl} from "./lib/config";
require('./locale')
import {initializeLocaleDropdown} from "./locale";
require('../css/cover.css')
require('../css/site.css')
initializeLocaleDropdown()
const options = {
setLoginStateChangeEvent(pageInit)
void pageInit()
async function pageInit() {
try {
const profile = await getLoginUserProfile()
$('.ui-signin').hide()
$('.ui-or').hide()
$('.ui-welcome').show()
if (profile.photo) $('.ui-avatar').prop('src', profile.photo).show()
else $('.ui-avatar').prop('src', '').hide()
$('.ui-name').html(profile.name)
$('.ui-signout').show()
navSection.historyPageBtn.trigger('click')
parseServerToHistory(historyList, parseHistoryCallback)
} catch (err) {
$('.ui-signin').show()
$('.ui-or').show()
$('.ui-welcome').hide()
$('.ui-avatar').prop('src', '').hide()
$('.ui-name').html('')
$('.ui-signout').hide()
parseStorageToHistory(historyList, parseHistoryCallback)
}
}
// prevent empty link change hash
$('a[href="#"]').on('click', function (e) {
e.preventDefault()
})
/**
* masthead nav section
*/
const navSection = {
introPageBtn: $('.ui-home'),
historyPageBtn: $('.ui-history'),
deleteUserModalCancel: $('.ui-delete-user-modal-cancel'),
logoutBtn: $('.ui-logout')
}
const introSection = $('#home')
const historySection = $('#history')
navSection.introPageBtn.on('click', function (e) {
if (!introSection.is(':visible')) {
$('.section:visible').hide()
introSection.fadeIn()
}
})
navSection.historyPageBtn.on('click', () => {
if (!historySection.is(':visible')) {
$('.section:visible').hide()
historySection.fadeIn()
}
})
navSection.deleteUserModalCancel.on('click', () => {
$('.ui-delete-user').parent().removeClass('active')
})
navSection.logoutBtn.on('click', () => {
clearLoginState()
location.href = `${serverurl}/logout`
})
/**
* History Section
*/
const options: List.ListOptions = {
valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'],
item: `<li class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
<span class="id" style="display:none;"></span>
@ -54,87 +131,169 @@ const options = {
</a>
</li>`,
page: 18,
pagination: [{
pagination: {
outerWindow: 1
}]
}
}
const historyList = new List('history', options)
window.migrateHistoryFromTempCallback = pageInit
setloginStateChangeEvent(pageInit)
pageInit()
function pageInit () {
checkIfAuth(
data => {
$('.ui-signin').hide()
$('.ui-or').hide()
$('.ui-welcome').show()
if (data.photo) $('.ui-avatar').prop('src', data.photo).show()
else $('.ui-avatar').prop('src', '').hide()
$('.ui-name').html(data.name)
$('.ui-signout').show()
$('.ui-history').click()
parseServerToHistory(historyList, parseHistoryCallback)
},
() => {
$('.ui-signin').show()
$('.ui-or').show()
$('.ui-welcome').hide()
$('.ui-avatar').prop('src', '').hide()
$('.ui-name').html('')
$('.ui-signout').hide()
parseStorageToHistory(historyList, parseHistoryCallback)
}
)
/**
* History Tool Bar
*/
const historyListContainer = $('#history-list')
const historyPagination = $('.pagination')
const historyToolbar = {
importFromBrowserBtn: $('.ui-import-from-browser'),
clearHistoryBtn: $('.ui-clear-history'),
saveHistoryBtn: $('.ui-save-history'),
openHistoryFile: $('.ui-open-history'),
tagFilter: $('.ui-use-tags')
}
$('.masthead-nav li').click(function () {
$(this).siblings().removeClass('active')
$(this).addClass('active')
/**
* clear all history
*/
historyToolbar.clearHistoryBtn.on('click', () => {
$('.ui-delete-history-modal-msg').text('Do you really want to clear all history?')
$('.ui-delete-history-modal-item').html('There is no turning back.')
isClearAllHistory = true
deleteHistoryModalTargetId = null
})
// prevent empty link change hash
$('a[href="#"]').click(function (e) {
e.preventDefault()
historyToolbar.importFromBrowserBtn.on('click', () => {
saveStorageHistoryToServer(() => {
parseStorageToHistory(historyList, parseHistoryCallback)
})
})
$('.ui-home').click(function (e) {
if (!$('#home').is(':visible')) {
$('.section:visible').hide()
$('#home').fadeIn()
}
historyToolbar.saveHistoryBtn.on('click', async function () {
const history = JSON.stringify(await getHistoryAsync())
const blob = new Blob([history], {
type: 'application/json;charset=utf-8'
})
saveAs(blob, `codimd_history_${moment().format('YYYYMMDDHHmmss')}`, true)
})
$('.ui-history').click(() => {
if (!$('#history').is(':visible')) {
$('.section:visible').hide()
$('#history').fadeIn()
}
})
function checkHistoryList () {
if ($('#history-list').children().length > 0) {
$('.pagination').show()
$('.ui-nohistory').hide()
$('.ui-import-from-browser').hide()
} else if ($('#history-list').children().length === 0) {
$('.pagination').hide()
$('.ui-nohistory').slideDown()
getStorageHistory(data => {
if (data && data.length > 0 && getLoginState() && historyList.items.length === 0) {
$('.ui-import-from-browser').slideDown()
}
historyToolbar.openHistoryFile.on('change', function (e) {
const files = e.target.files || e.dataTransfer.files
const file = files[0]
const reader = new FileReader()
reader.onload = () => {
const notehistory = JSON.parse(reader.result)
// console.log(notehistory);
if (!reader.result) return
getHistory(data => {
let mergedata = data.concat(notehistory)
mergedata = clearDuplicatedHistory(mergedata)
saveHistory(mergedata)
parseHistory(historyList, parseHistoryCallback)
})
historyToolbar.openHistoryFile.replaceWith(historyToolbar.openHistoryFile.val('').clone(true))
}
reader.readAsText(file)
})
$('.ui-refresh-history').on('click', () => {
const lastTags = historyToolbar.tagFilter.select2('val')
historyToolbar.tagFilter.select2('val', '')
historyList.filter()
const lastKeyword = $('.search').val()
$('.search').val('')
historyList.search()
historyListContainer.slideUp('fast')
historyPagination.hide()
resetCheckAuth()
historyList.clear()
parseHistory(historyList, (list, notehistory) => {
parseHistoryCallback(list, notehistory)
historyToolbar.tagFilter.select2('val', lastTags)
historyToolbar.tagFilter.trigger('change')
historyList.search(lastKeyword)
$('.search').val(lastKeyword)
refreshHistoryUIElementVisibility()
historyListContainer.slideDown('fast')
})
})
let filtertags = []
historyToolbar.tagFilter.select2({
placeholder: historyToolbar.tagFilter.attr('placeholder'),
multiple: true,
data() {
return {
results: filtertags
}
}
})
$('.select2-input').css('width', 'inherit')
buildTagsFilter([])
function buildTagsFilter(tags) {
for (let i = 0; i < tags.length; i++) {
tags[i] = {
id: i,
text: unescapeHTML(tags[i])
}
}
filtertags = tags
}
function parseHistoryCallback (list, notehistory) {
checkHistoryList()
historyToolbar.tagFilter.on('change', function () {
const tags = []
const data = $(this).select2('data')
for (let i = 0; i < data.length; i++) {
tags.push(data[i].text)
}
if (tags.length > 0) {
historyList.filter(item => {
const values = item.values()
if (!values.tags) return false
let found = false
for (let i = 0; i < tags.length; i++) {
if (values.tags.includes(tags[i])) {
found = true
break
}
}
return found
})
} else {
historyList.filter()
}
refreshHistoryUIElementVisibility()
})
$('.search').on('keyup', () => {
refreshHistoryUIElementVisibility()
})
// ----------------------------------------------------------------------------------------------------------------
function refreshHistoryUIElementVisibility() {
if (historyListContainer.children().length > 0) {
historyPagination.show()
$('.ui-nohistory').hide()
historyToolbar.importFromBrowserBtn.hide()
return
}
historyPagination.hide()
$('.ui-nohistory').slideDown()
getStorageHistory(data => {
if (data && data.length > 0 && getLoginState() && historyList.items.length === 0) {
historyToolbar.importFromBrowserBtn.slideDown()
}
})
}
function parseHistoryCallback(list, notehistory) {
refreshHistoryUIElementVisibility()
// sort by pinned then timestamp
list.sort('', {
sortFunction (a, b) {
sortFunction(a, b) {
const notea = a.values()
const noteb = b.values()
if (notea.pinned && !noteb.pinned) {
@ -160,8 +319,12 @@ function parseHistoryCallback (list, notehistory) {
for (let j = 0; j < tags.length; j++) {
// push info filtertags if not found
let found = false
if (filtertags.includes(tags[j])) { found = true }
if (!found) { filtertags.push(tags[j]) }
if (filtertags.includes(tags[j])) {
found = true
}
if (!found) {
filtertags.push(tags[j])
}
}
}
}
@ -169,7 +332,7 @@ function parseHistoryCallback (list, notehistory) {
}
// update items whenever list updated
historyList.on('updated', e => {
historyList.on('updated', function (e) {
for (let i = 0, l = e.items.length; i < l; i++) {
const item = e.items[i]
if (item.visible()) {
@ -189,7 +352,7 @@ historyList.on('updated', e => {
// parse tags
const tags = values.tags
if (tags && tags.length > 0 && tagsEl.children().length <= 0) {
const labels = []
const labels: string[] = []
for (let j = 0; j < tags.length; j++) {
// push into the item label
labels.push(`<span class='label label-default'>${tags[j]}</span>`)
@ -199,22 +362,66 @@ historyList.on('updated', e => {
}
}
$('.ui-history-close').off('click')
$('.ui-history-close').on('click', historyCloseClick)
$('.ui-history-close').on('click', onHistoryCloseClick)
$('.ui-history-pin').off('click')
$('.ui-history-pin').on('click', historyPinClick)
})
function historyCloseClick (e) {
/**
* Delete History Modal
*/
let isClearAllHistory: boolean = false
let deleteHistoryModalTargetId: string | null = null
function onHistoryCloseClick(this: HTMLElement, e) {
e.preventDefault()
const id = $(this).closest('a').siblings('span').html()
const value = historyList.get('id', id)[0]._values
const id: string = $(this).closest('a').siblings('span').html()
const value: NoteHistory = historyList.get('id', id)[0].values()
$('.ui-delete-history-modal-msg').text('Do you really want to delete below history?')
$('.ui-delete-history-modal-item').html(`<i class="fa fa-file-text"></i> ${value.text}<br><i class="fa fa-clock-o"></i> ${value.time}`)
clearHistory = false
deleteId = id
isClearAllHistory = false
deleteHistoryModalTargetId = id
}
function historyPinClick (e) {
$('.ui-delete-history-modal-confirm').on('click', async function () {
if (await isLogin()) {
if (isClearAllHistory) {
await clearServerHistoryAsync()
} else {
if (deleteHistoryModalTargetId) {
await deleteServerHistoryAsync(deleteHistoryModalTargetId)
}
}
refreshHistoryUIElementVisibility()
$('.delete-history-modal').modal('hide')
deleteHistoryModalTargetId = null
isClearAllHistory = false
} else {
if (isClearAllHistory) {
saveHistory([])
historyList.clear()
refreshHistoryUIElementVisibility()
deleteHistoryModalTargetId = null
} else {
if (!deleteHistoryModalTargetId) return
getHistory(notehistory => {
const newnotehistory = removeHistory(deleteHistoryModalTargetId, notehistory)
saveHistory(newnotehistory)
historyList.remove('id', deleteHistoryModalTargetId)
refreshHistoryUIElementVisibility()
deleteHistoryModalTargetId = null
})
}
$('.delete-history-modal').modal('hide')
isClearAllHistory = false
}
})
/**
* Pin History
*/
function historyPinClick(e) {
e.preventDefault()
const $this = $(this)
const id = $this.closest('a').siblings('span').html()
@ -229,13 +436,17 @@ function historyPinClick (e) {
item._values.pinned = false
}
checkIfAuth(() => {
postHistoryToServer(id, {
pinned
}, (err, result) => {
if (!err) {
if (pinned) { $this.addClass('active') } else { $this.removeClass('active') }
}
})
postHistoryToServerAsync(id, {pinned})
.then(() => {
if (pinned) {
$this.addClass('active')
} else {
$this.removeClass('active')
}
})
.catch(err => {
console.error(err)
})
}, () => {
getHistory(notehistory => {
for (let i = 0; i < notehistory.length; i++) {
@ -245,7 +456,11 @@ function historyPinClick (e) {
}
}
saveHistory(notehistory)
if (pinned) { $this.addClass('active') } else { $this.removeClass('active') }
if (pinned) {
$this.addClass('active')
} else {
$this.removeClass('active')
}
})
})
}
@ -253,7 +468,7 @@ function historyPinClick (e) {
// auto update item fromNow every minutes
setInterval(updateItemFromNow, 60000)
function updateItemFromNow () {
function updateItemFromNow() {
const items = $('.item').toArray()
for (let i = 0; i < items.length; i++) {
const item = $(items[i])
@ -261,170 +476,3 @@ function updateItemFromNow () {
item.find('.fromNow').text(moment(timestamp).fromNow())
}
}
var clearHistory = false
var deleteId = null
function deleteHistory () {
checkIfAuth(() => {
deleteServerHistory(deleteId, (err, result) => {
if (!err) {
if (clearHistory) {
historyList.clear()
checkHistoryList()
} else {
historyList.remove('id', deleteId)
checkHistoryList()
}
}
$('.delete-history-modal').modal('hide')
deleteId = null
clearHistory = false
})
}, () => {
if (clearHistory) {
saveHistory([])
historyList.clear()
checkHistoryList()
deleteId = null
} else {
if (!deleteId) return
getHistory(notehistory => {
const newnotehistory = removeHistory(deleteId, notehistory)
saveHistory(newnotehistory)
historyList.remove('id', deleteId)
checkHistoryList()
deleteId = null
})
}
$('.delete-history-modal').modal('hide')
clearHistory = false
})
}
$('.ui-delete-history-modal-confirm').click(() => {
deleteHistory()
})
$('.ui-import-from-browser').click(() => {
saveStorageHistoryToServer(() => {
parseStorageToHistory(historyList, parseHistoryCallback)
})
})
$('.ui-save-history').click(() => {
getHistory(data => {
const history = JSON.stringify(data)
const blob = new Blob([history], {
type: 'application/json;charset=utf-8'
})
saveAs(blob, `codimd_history_${moment().format('YYYYMMDDHHmmss')}`, true)
})
})
$('.ui-open-history').bind('change', e => {
const files = e.target.files || e.dataTransfer.files
const file = files[0]
const reader = new FileReader()
reader.onload = () => {
const notehistory = JSON.parse(reader.result)
// console.log(notehistory);
if (!reader.result) return
getHistory(data => {
let mergedata = data.concat(notehistory)
mergedata = clearDuplicatedHistory(mergedata)
saveHistory(mergedata)
parseHistory(historyList, parseHistoryCallback)
})
$('.ui-open-history').replaceWith($('.ui-open-history').val('').clone(true))
}
reader.readAsText(file)
})
$('.ui-clear-history').click(() => {
$('.ui-delete-history-modal-msg').text('Do you really want to clear all history?')
$('.ui-delete-history-modal-item').html('There is no turning back.')
clearHistory = true
deleteId = null
})
$('.ui-refresh-history').click(() => {
const lastTags = $('.ui-use-tags').select2('val')
$('.ui-use-tags').select2('val', '')
historyList.filter()
const lastKeyword = $('.search').val()
$('.search').val('')
historyList.search()
$('#history-list').slideUp('fast')
$('.pagination').hide()
resetCheckAuth()
historyList.clear()
parseHistory(historyList, (list, notehistory) => {
parseHistoryCallback(list, notehistory)
$('.ui-use-tags').select2('val', lastTags)
$('.ui-use-tags').trigger('change')
historyList.search(lastKeyword)
$('.search').val(lastKeyword)
checkHistoryList()
$('#history-list').slideDown('fast')
})
})
$('.ui-delete-user-modal-cancel').click(() => {
$('.ui-delete-user').parent().removeClass('active')
})
$('.ui-logout').click(() => {
clearLoginState()
location.href = `${serverurl}/logout`
})
let filtertags = []
$('.ui-use-tags').select2({
placeholder: $('.ui-use-tags').attr('placeholder'),
multiple: true,
data () {
return {
results: filtertags
}
}
})
$('.select2-input').css('width', 'inherit')
buildTagsFilter([])
function buildTagsFilter (tags) {
for (let i = 0; i < tags.length; i++) {
tags[i] = {
id: i,
text: unescapeHTML(tags[i])
}
}
filtertags = tags
}
$('.ui-use-tags').on('change', function () {
const tags = []
const data = $(this).select2('data')
for (let i = 0; i < data.length; i++) { tags.push(data[i].text) }
if (tags.length > 0) {
historyList.filter(item => {
const values = item.values()
if (!values.tags) return false
let found = false
for (let i = 0; i < tags.length; i++) {
if (values.tags.includes(tags[i])) {
found = true
break
}
}
return found
})
} else {
historyList.filter()
}
checkHistoryList()
})
$('.search').keyup(() => {
checkHistoryList()
})

View File

@ -44,6 +44,8 @@ require('prismjs/components/prism-gherkin')
require('./lib/common/login')
require('../vendor/md-toc')
let viz = new window.Viz()
const plantumlEncoder = require('plantuml-encoder')

View File

@ -1,9 +1,10 @@
/* eslint-env browser, jquery */
/* global serverurl, moment */
import $ from 'jquery'
import store from 'store'
import LZString from '@hackmd/lz-string'
import {serverurl} from "./lib/config";
import escapeHTML from 'lodash/escape'
import {
@ -11,13 +12,11 @@ import {
encodeNoteId
} from './utils'
import { checkIfAuth } from './lib/common/login'
import {checkIfAuth, isLogin} from './lib/common/login'
import { urlpath } from './lib/config'
import {urlpath} from './lib/config'
window.migrateHistoryFromTempCallback = null
export function saveHistory (notehistory) {
export function saveHistory(notehistory) {
checkIfAuth(
() => {
saveHistoryToServer(notehistory)
@ -28,17 +27,17 @@ export function saveHistory (notehistory) {
)
}
function saveHistoryToStorage (notehistory) {
function saveHistoryToStorage(notehistory) {
store.set('notehistory', JSON.stringify(notehistory))
}
function saveHistoryToServer (notehistory) {
function saveHistoryToServer(notehistory) {
$.post(`${serverurl}/history`, {
history: JSON.stringify(notehistory)
})
}
export function saveStorageHistoryToServer (callback) {
export function saveStorageHistoryToServer(callback) {
const data = store.get('notehistory')
if (data) {
$.post(`${serverurl}/history`, {
@ -50,7 +49,7 @@ export function saveStorageHistoryToServer (callback) {
}
}
export function clearDuplicatedHistory (notehistory) {
export function clearDuplicatedHistory(notehistory) {
const newnotehistory = []
for (let i = 0; i < notehistory.length; i++) {
let found = false
@ -67,12 +66,14 @@ export function clearDuplicatedHistory (notehistory) {
break
}
}
if (!found) { newnotehistory.push(notehistory[i]) }
if (!found) {
newnotehistory.push(notehistory[i])
}
}
return newnotehistory
}
function addHistory (id, text, time, tags, pinned, notehistory) {
function addHistory(id, text, time, tags, pinned, notehistory) {
// only add when note id exists
if (id) {
notehistory.push({
@ -86,7 +87,7 @@ function addHistory (id, text, time, tags, pinned, notehistory) {
return notehistory
}
export function removeHistory (id, notehistory) {
export function removeHistory(id, notehistory) {
for (let i = 0; i < notehistory.length; i++) {
if (notehistory[i].id === id) {
notehistory.splice(i, 1)
@ -97,7 +98,7 @@ export function removeHistory (id, notehistory) {
}
// used for inner
export function writeHistory (title, tags) {
export function writeHistory(title, tags) {
checkIfAuth(
() => {
// no need to do this anymore, this will count from server-side
@ -109,7 +110,7 @@ export function writeHistory (title, tags) {
)
}
function writeHistoryToStorage (title, tags) {
function writeHistoryToStorage(title, tags) {
const data = store.get('notehistory')
let notehistory
if (data && typeof data === 'string') {
@ -126,7 +127,7 @@ if (!Array.isArray) {
Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]'
}
function renderHistory (title, tags) {
function renderHistory(title, tags) {
// console.debug(tags);
const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1]
return {
@ -137,7 +138,7 @@ function renderHistory (title, tags) {
}
}
function generateHistory (title, tags, notehistory) {
function generateHistory(title, tags, notehistory) {
const info = renderHistory(title, tags)
// keep any pinned data
let pinned = false
@ -154,22 +155,42 @@ function generateHistory (title, tags, notehistory) {
}
// used for outer
export function getHistory (callback) {
checkIfAuth(
() => {
getServerHistory(callback)
},
() => {
getStorageHistory(callback)
}
)
export async function getHistoryAsync(): Promise<NoteHistory[]> {
if (await isLogin()) {
return await getServerHistoryAsync()
}
return getStorageHistoryAsync();
}
function getServerHistory (callback) {
export function getHistory(callback: (_: NoteHistory[]) => void) {
getHistoryAsync()
.then((data) => {
callback(data)
})
}
export interface NoteHistory {
id?: string
text?: string
time?: Date | number
tags?: string[]
pinned?: boolean
}
function getServerHistoryAsync(): Promise<NoteHistory[]> {
return new Promise((resolve) => {
getServerHistory(function (data: NoteHistory[]) {
resolve(data)
})
})
}
function getServerHistory(callback) {
$.get(`${serverurl}/history`)
.done(data => {
if (data.history) {
callback(data.history)
callback(data.history as NoteHistory[])
}
})
.fail((xhr, status, error) => {
@ -177,17 +198,30 @@ function getServerHistory (callback) {
})
}
export function getStorageHistory (callback) {
function getStorageHistoryAsync(): NoteHistory[] {
let data = store.get('notehistory')
if (data) {
if (typeof data === 'string') { data = JSON.parse(data) }
if (typeof data === 'string') {
data = JSON.parse(data)
}
return data
}
return []
}
export function getStorageHistory(callback) {
let data = store.get('notehistory')
if (data) {
if (typeof data === 'string') {
data = JSON.parse(data)
}
callback(data)
}
// eslint-disable-next-line standard/no-callback-literal
callback([])
}
export function parseHistory (list, callback) {
export function parseHistory(list, callback) {
checkIfAuth(
() => {
parseServerToHistory(list, callback)
@ -198,7 +232,7 @@ export function parseHistory (list, callback) {
)
}
export function parseServerToHistory (list, callback) {
export function parseServerToHistory(list, callback) {
$.get(`${serverurl}/history`)
.done(data => {
if (data.history) {
@ -210,16 +244,18 @@ export function parseServerToHistory (list, callback) {
})
}
export function parseStorageToHistory (list, callback) {
export function parseStorageToHistory(list, callback) {
let data = store.get('notehistory')
if (data) {
if (typeof data === 'string') { data = JSON.parse(data) }
if (typeof data === 'string') {
data = JSON.parse(data)
}
parseToHistory(list, data, callback)
}
parseToHistory(list, [], callback)
}
function parseToHistory (list, notehistory, callback) {
function parseToHistory(list, notehistory, callback) {
if (!callback) return
else if (!list || !notehistory) callback(list, notehistory)
else if (notehistory && notehistory.length > 0) {
@ -242,29 +278,53 @@ function parseToHistory (list, notehistory, callback) {
notehistory[i].text = escapeHTML(notehistory[i].text)
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHTML(notehistory[i].tags).split(',') : []
// add to list
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) {
list.add(notehistory[i])
}
}
}
callback(list, notehistory)
}
export function postHistoryToServer (noteId, data, callback) {
$.post(`${serverurl}/history/${noteId}`, data)
.done(result => callback(null, result))
.fail((xhr, status, error) => {
console.error(xhr.responseText)
return callback(error, null)
})
interface HistoryUpdatePayload {
pinned: boolean
}
export function deleteServerHistory (noteId, callback) {
$.ajax({
url: `${serverurl}/history${noteId ? '/' + noteId : ''}`,
type: 'DELETE'
export function postHistoryToServerAsync(noteId: string, data ?: HistoryUpdatePayload) {
return new Promise((resolve, reject) => {
$.post(`${serverurl}/history/${noteId}`, data as any)
.done(result => resolve(result))
.fail((xhr, status, error) => {
console.error(xhr.responseText)
return reject(error)
})
})
}
export function deleteServerHistoryAsync(noteId: string) {
return new Promise((resolve, reject) => {
$.ajax({
url: `${serverurl}/history/${noteId}`,
type: 'DELETE'
})
.done(result => resolve(result))
.fail((xhr, status, error) => {
console.error(xhr.responseText)
return reject(error)
})
})
}
export function clearServerHistoryAsync() {
return new Promise((resolve, reject) => {
$.ajax({
url: `${serverurl}/history`,
type: 'DELETE'
})
.done(result => resolve(result))
.fail((xhr, status, error) => {
console.error(xhr.responseText)
return reject(error)
})
})
.done(result => callback(null, result))
.fail((xhr, status, error) => {
console.error(xhr.responseText)
return callback(error, null)
})
}

View File

@ -23,14 +23,14 @@ import { Spinner } from 'spin.js'
import {
checkLoginStateChanged,
setloginStateChangeEvent
setLoginStateChangeEvent
} from './lib/common/login'
import {
debug,
DROPBOX_APP_KEY,
noteid,
noteurl,
noteurl, serverurl,
urlpath,
version
} from './lib/config'
@ -66,10 +66,9 @@ import {
import {
writeHistory,
deleteServerHistory,
getHistory,
saveHistory,
removeHistory
removeHistory, deleteServerHistoryAsync
} from './history'
import { preventXSS } from './render'
@ -396,7 +395,7 @@ function setNeedRefresh () {
showStatus(statusType.offline)
}
setloginStateChangeEvent(function () {
setLoginStateChangeEvent(function () {
setRefreshModal('user-state-changed')
setNeedRefresh()
})
@ -1752,11 +1751,14 @@ socket.on('error', function (data) {
console.error(data)
if (data.message && data.message.indexOf('AUTH failed') === 0) { location.href = serverurl + '/403' }
})
socket.on('delete', function () {
socket.on('delete', async function () {
if (personalInfo.login) {
deleteServerHistory(noteid, function (err, data) {
if (!err) location.href = serverurl
})
try {
await deleteServerHistoryAsync(noteid)
location.href = serverurl
} catch (err) {
console.error(err)
}
} else {
getHistory(function (notehistory) {
var newnotehistory = removeHistory(noteid, notehistory)

View File

@ -1,24 +1,28 @@
/* eslint-env browser, jquery */
/* global Cookies */
import { serverurl } from '../config'
import Cookies from 'js-cookie'
let checkAuth = false
import {serverurl} from '../config'
type PureFunction = () => void
let checkAuth: boolean = false
let profile = null
let lastLoginState = getLoginState()
let lastUserId = getUserId()
let loginStateChangeEvent = null
let lastUserId: string | null = getUserId()
let loginStateChangeEvent: PureFunction | null = null
export function setloginStateChangeEvent (func) {
export function setLoginStateChangeEvent(func: PureFunction): void {
loginStateChangeEvent = func
}
export function resetCheckAuth () {
export function resetCheckAuth(): void {
checkAuth = false
}
export function setLoginState (bool, id) {
Cookies.set('loginstate', bool, {
function setLoginState(bool: boolean, id?: string) {
Cookies.set('loginstate', String(bool), {
expires: 365
})
if (id) {
@ -29,64 +33,81 @@ export function setLoginState (bool, id) {
Cookies.remove('userid')
}
lastLoginState = bool
lastUserId = id
lastUserId = id || null
checkLoginStateChanged()
}
export function checkLoginStateChanged () {
export function checkLoginStateChanged(): boolean {
if (getLoginState() !== lastLoginState || getUserId() !== lastUserId) {
if (loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100)
return true
} else {
}
return false
}
export function getLoginState(): boolean {
const state = Cookies.get('loginstate')
return state === 'true'
}
function getUserId(): string {
return Cookies.get('userid') || ""
}
export function clearLoginState(): void {
Cookies.remove('loginstate')
}
type YesCallbackFunc = (profile: any) => void
/**
* getLoginUserProfile
* @throws Error when user not login
* @return profile user profile
*/
export async function getLoginUserProfile(): Promise<any> {
return new Promise(function (resolve, reject) {
const cookieLoginState = getLoginState()
if (checkLoginStateChanged()) checkAuth = false
if (!checkAuth || typeof cookieLoginState === 'undefined') {
$.get(`${serverurl}/me`)
.done(data => {
if (data && data.status === 'ok') {
profile = data
setLoginState(true, data.id)
return resolve(profile)
} else {
setLoginState(false)
return reject(new Error('user not login'))
}
})
.fail(() => {
return reject(new Error('user not login'))
})
.always(() => {
checkAuth = true
})
} else if (cookieLoginState) {
return resolve(profile)
} else {
return reject(new Error('user not login'))
}
})
}
export async function isLogin(): Promise<boolean> {
try {
await getLoginUserProfile()
return true
} catch (e) {
return false
}
}
export function getLoginState () {
const state = Cookies.get('loginstate')
return state === 'true' || state === true
}
export function getUserId () {
return Cookies.get('userid')
}
export function clearLoginState () {
Cookies.remove('loginstate')
}
export function checkIfAuth (yesCallback, noCallback) {
const cookieLoginState = getLoginState()
if (checkLoginStateChanged()) checkAuth = false
if (!checkAuth || typeof cookieLoginState === 'undefined') {
$.get(`${serverurl}/me`)
.done(data => {
if (data && data.status === 'ok') {
profile = data
yesCallback(profile)
setLoginState(true, data.id)
} else {
noCallback()
setLoginState(false)
}
})
.fail(() => {
noCallback()
})
.always(() => {
checkAuth = true
})
} else if (cookieLoginState) {
export function checkIfAuth(yesCallback: YesCallbackFunc, noCallback: PureFunction) {
getLoginUserProfile().then((profile) => {
yesCallback(profile)
} else {
}).catch(() => {
noCallback()
}
}
export default {
checkAuth,
profile,
lastLoginState,
lastUserId,
loginStateChangeEvent
})
}

View File

@ -1,12 +1,3 @@
declare interface Window {
DROPBOX_APP_KEY: string,
domain: string,
urlpath: string,
debug: boolean,
serverurl: string,
version: string
USE_CDN: boolean
}
export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || ''

View File

@ -1,31 +1,48 @@
/* eslint-env browser, jquery */
/* global Cookies */
var lang = 'en'
var userLang = navigator.language || navigator.userLanguage
var userLangCode = userLang.split('-')[0]
var locale = $('.ui-locale')
var supportLangs = []
$('.ui-locale option').each(function () {
supportLangs.push($(this).val())
})
if (Cookies.get('locale')) {
lang = Cookies.get('locale')
if (lang === 'zh') {
lang = 'zh-TW'
import $ from 'jquery'
import Cookies from 'js-cookie'
const userLang = navigator.language || navigator.userLanguage
const userLangCode = userLang.split('-')[0]
let lang = 'en'
let supportLangs: string[] = []
function reloadLanguageFromCookie () {
if (Cookies.get('locale')) {
lang = Cookies.get('locale') as string
if (lang === 'zh') {
lang = 'zh-TW'
}
} else if (supportLangs.indexOf(userLang) !== -1) {
lang = supportLangs[supportLangs.indexOf(userLang)]
} else if (supportLangs.indexOf(userLangCode) !== -1) {
lang = supportLangs[supportLangs.indexOf(userLangCode)]
}
} else if (supportLangs.indexOf(userLang) !== -1) {
lang = supportLangs[supportLangs.indexOf(userLang)]
} else if (supportLangs.indexOf(userLangCode) !== -1) {
lang = supportLangs[supportLangs.indexOf(userLangCode)]
}
locale.val(lang)
$('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected')
locale.change(function () {
Cookies.set('locale', $(this).val(), {
expires: 365
function fillSupportLanguageArrayFromDropdownOptions () {
const $langOptions = $('.ui-locale option');
$langOptions.each(function () {
supportLangs.push($(this).val() as string)
})
window.location.reload()
})
}
export function initializeLocaleDropdown() {
const $locale = $('.ui-locale')
fillSupportLanguageArrayFromDropdownOptions()
reloadLanguageFromCookie()
$locale.val(lang)
$('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected')
$locale.on('change', function () {
Cookies.set('locale', $(this).val() as string, {
expires: 365
})
window.location.reload()
})
}

View File

@ -1,16 +1,15 @@
/* eslint-env browser, jquery */
// allow some attributes
var filterXSS = require('xss')
import {filterXSS, escapeAttrValue, whiteList} from 'xss'
var whiteListAttr = ['id', 'class', 'style']
const whiteListAttr = ['id', 'class', 'style']
window.whiteListAttr = whiteListAttr
// allow link starts with '.', '/' and custom protocol with '://', exclude link starts with javascript://
var linkRegex = /^(?!javascript:\/\/)([\w|-]+:\/\/)|^([.|/])+/i
const linkRegex = /^(?!javascript:\/\/)([\w|-]+:\/\/)|^([.|/])+/i
// allow data uri, from https://gist.github.com/bgrins/6194623
var dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i
const dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i
// custom white list
var whiteList = filterXSS.whiteList
// allow ol specify start number
whiteList.ol = ['start']
// allow li specify value number
@ -34,7 +33,7 @@ whiteList.figure = []
// allow figcaption tag
whiteList.figcaption = []
var filterXSSOptions = {
const filterXSSOptions = {
allowCommentTag: true,
whiteList: whiteList,
escapeHtml: function (html) {
@ -51,28 +50,25 @@ var filterXSSOptions = {
onTagAttr: function (tag, name, value, isWhiteAttr) {
// allow href and src that match linkRegex
if (isWhiteAttr && (name === 'href' || name === 'src') && linkRegex.test(value)) {
return name + '="' + filterXSS.escapeAttrValue(value) + '"'
return name + '="' + escapeAttrValue(value) + '"'
}
// allow data uri in img src
if (isWhiteAttr && (tag === 'img' && name === 'src') && dataUriRegex.test(value)) {
return name + '="' + filterXSS.escapeAttrValue(value) + '"'
return name + '="' + escapeAttrValue(value) + '"'
}
},
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
// allow attr start with 'data-' or in the whiteListAttr
if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) {
// escape its value using built-in escapeAttrValue function
return name + '="' + filterXSS.escapeAttrValue(value) + '"'
return name + '="' + escapeAttrValue(value) + '"'
}
}
}
function preventXSS (html) {
export function preventXSS (html) {
return filterXSS(html, filterXSSOptions)
}
window.preventXSS = preventXSS
module.exports = {
preventXSS: preventXSS,
escapeAttrValue: filterXSS.escapeAttrValue
}
export {escapeAttrValue} from 'xss'

27
public/js/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"sourceMap": true,
"baseUrl": ".",
"allowJs": true,
"strict": false,
"strictNullChecks": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"alwaysStrict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["../../node_modules/@types",
"./typings"
]
},
"include": [
"./"
]
}

12
public/js/typings/index.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare global {
interface Window {
Viz: any
createtime: any
lastchangetime: any
lastchangeui: any
owner: any
ownerprofile: any
whiteListAttr: any
preventXSS: any
}
}

View File

@ -2,23 +2,23 @@ import base64url from 'base64url'
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
export function checkNoteIdValid (id) {
export function checkNoteIdValid (id: string) {
const result = id.match(uuidRegex)
return !!(result && result.length === 1)
}
export function encodeNoteId (id) {
export function encodeNoteId (id: string) {
// remove dashes in UUID and encode in url-safe base64
const str = id.replace(/-/g, '')
const hexStr = Buffer.from(str, 'hex')
return base64url.encode(hexStr)
}
export function decodeNoteId (encodedId) {
export function decodeNoteId (encodedId: string) {
// decode from url-safe base64
const id = base64url.toBuffer(encodedId).toString('hex')
// add dashes between the UUID string parts
const idParts = []
const idParts: string[] = []
idParts.push(id.substr(0, 8))
idParts.push(id.substr(8, 4))
idParts.push(id.substr(12, 4))