From f83b4b77fdc14006a6542bcde815ecd8d926a5e1 Mon Sep 17 00:00:00 2001 From: Raccoon Date: Sat, 31 Jul 2021 15:56:50 +0800 Subject: [PATCH] refactor --- package-lock.json | 58 +++- package.json | 7 +- public/js/cover.ts | 584 ++++++++++++++++++---------------- public/js/extra.ts | 2 + public/js/history.ts | 164 +++++++--- public/js/index.ts | 20 +- public/js/lib/common/login.ts | 133 ++++---- public/js/lib/config/index.ts | 9 - public/js/locale.ts | 65 ++-- public/js/render.ts | 24 +- public/js/tsconfig.json | 27 ++ public/js/typings/index.d.ts | 12 + public/js/utils.ts | 8 +- 13 files changed, 672 insertions(+), 441 deletions(-) create mode 100644 public/js/tsconfig.json create mode 100644 public/js/typings/index.d.ts diff --git a/package-lock.json b/package-lock.json index 90a132c0..27f548b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index b1c14f3f..039f6afc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/js/cover.ts b/public/js/cover.ts index 10111bfa..d91ba597 100644 --- a/public/js/cover.ts +++ b/public/js/cover.ts @@ -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: `
  • @@ -54,87 +131,169 @@ const options = {
  • `, 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(`${tags[j]}`) @@ -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(` ${value.text}
    ${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() -}) diff --git a/public/js/extra.ts b/public/js/extra.ts index f076d1bb..84722051 100644 --- a/public/js/extra.ts +++ b/public/js/extra.ts @@ -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') diff --git a/public/js/history.ts b/public/js/history.ts index da39f095..945d6a14 100644 --- a/public/js/history.ts +++ b/public/js/history.ts @@ -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 { + 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 { + 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) - }) } diff --git a/public/js/index.ts b/public/js/index.ts index 82cbb357..9213ea4e 100644 --- a/public/js/index.ts +++ b/public/js/index.ts @@ -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) diff --git a/public/js/lib/common/login.ts b/public/js/lib/common/login.ts index 15c64668..770ccd9a 100644 --- a/public/js/lib/common/login.ts +++ b/public/js/lib/common/login.ts @@ -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 { + 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 { + 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 + }) } diff --git a/public/js/lib/config/index.ts b/public/js/lib/config/index.ts index 89fe0bfb..04c631ba 100644 --- a/public/js/lib/config/index.ts +++ b/public/js/lib/config/index.ts @@ -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 || '' diff --git a/public/js/locale.ts b/public/js/locale.ts index 71c0f99f..89a93820 100644 --- a/public/js/locale.ts +++ b/public/js/locale.ts @@ -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() + }) +} diff --git a/public/js/render.ts b/public/js/render.ts index 4a9c3b25..66ce713d 100644 --- a/public/js/render.ts +++ b/public/js/render.ts @@ -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' diff --git a/public/js/tsconfig.json b/public/js/tsconfig.json new file mode 100644 index 00000000..1ab62d9e --- /dev/null +++ b/public/js/tsconfig.json @@ -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": [ + "./" + ] +} diff --git a/public/js/typings/index.d.ts b/public/js/typings/index.d.ts new file mode 100644 index 00000000..51da1516 --- /dev/null +++ b/public/js/typings/index.d.ts @@ -0,0 +1,12 @@ +declare global { + interface Window { + Viz: any + createtime: any + lastchangetime: any + lastchangeui: any + owner: any + ownerprofile: any + whiteListAttr: any + preventXSS: any + } +} diff --git a/public/js/utils.ts b/public/js/utils.ts index 6a98ce9e..82183cbc 100644 --- a/public/js/utils.ts +++ b/public/js/utils.ts @@ -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))