From 53ba5b7e33135cfdcaa19ead8c226c653a78b9a7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 8 Jun 2019 00:02:51 +0100 Subject: [PATCH] Removed jQuery and replaced axios with fetch --- package-lock.json | 42 +---- package.json | 2 - readme.md | 2 - .../js/components/breadcrumb-listing.js | 2 +- .../assets/js/components/page-comments.js | 8 +- .../assets/js/components/page-display.js | 15 +- resources/assets/js/index.js | 20 +-- resources/assets/js/services/dom-polyfills.js | 22 --- resources/assets/js/services/global-ui.js | 58 ------- resources/assets/js/services/http.js | 157 ++++++++++++++++-- resources/assets/js/services/util.js | 23 ++- .../assets/js/vues/components/autosuggest.js | 8 +- resources/assets/js/vues/image-manager.js | 4 +- 13 files changed, 194 insertions(+), 169 deletions(-) delete mode 100644 resources/assets/js/services/dom-polyfills.js delete mode 100644 resources/assets/js/services/global-ui.js diff --git a/package-lock.json b/package-lock.json index f234d4f9d..352ad5ce9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -441,22 +441,6 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, - "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", - "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - } - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1855,24 +1839,6 @@ } } }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2570,11 +2536,6 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" - }, "js-base64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", @@ -3082,7 +3043,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "nan": { "version": "2.14.0", diff --git a/package.json b/package.json index 0198f0ccc..0d1afbb0c 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,9 @@ "webpack-cli": "^3.3.2" }, "dependencies": { - "axios": "^0.19.0", "clipboard": "^2.0.4", "codemirror": "^5.47.0", "dropzone": "^5.5.1", - "jquery": "^3.4.1", "markdown-it": "^8.4.2", "markdown-it-task-lists": "^2.1.1", "sortablejs": "^1.9.0", diff --git a/readme.md b/readme.md index 5c400a0c9..07bebff05 100644 --- a/readme.md +++ b/readme.md @@ -137,11 +137,9 @@ The great people that have worked to build and improve BookStack can [be seen he These are the great open-source projects used to help build BookStack: * [Laravel](http://laravel.com/) -* [jQuery](https://jquery.com/) * [TinyMCE](https://www.tinymce.com/) * [CodeMirror](https://codemirror.net) * [Vue.js](http://vuejs.org/) -* [Axios](https://github.com/mzabriskie/axios) * [Sortable](https://github.com/SortableJS/Sortable) & [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable) * [Google Material Icons](https://material.io/icons/) * [Dropzone.js](http://www.dropzonejs.com/) diff --git a/resources/assets/js/components/breadcrumb-listing.js b/resources/assets/js/components/breadcrumb-listing.js index 44f7d89e0..11e1522db 100644 --- a/resources/assets/js/components/breadcrumb-listing.js +++ b/resources/assets/js/components/breadcrumb-listing.js @@ -60,7 +60,7 @@ class BreadcrumbListing { 'entity_type': this.entityType, }; - window.$http.get('/search/entity/siblings', {params}).then(resp => { + window.$http.get('/search/entity/siblings', params).then(resp => { this.entityListElem.innerHTML = resp.data; }).catch(err => { console.error(err); diff --git a/resources/assets/js/components/page-comments.js b/resources/assets/js/components/page-comments.js index 975ff5a82..cabce9139 100644 --- a/resources/assets/js/components/page-comments.js +++ b/resources/assets/js/components/page-comments.js @@ -1,4 +1,6 @@ import MarkdownIt from "markdown-it"; +import {scrollAndHighlightElement} from "../services/util"; + const md = new MarkdownIt({ html: false }); class PageComments { @@ -25,8 +27,8 @@ class PageComments { handleAction(event) { let actionElem = event.target.closest('[action]'); if (event.target.matches('a[href^="#"]')) { - let id = event.target.href.split('#')[1]; - window.scrollAndHighlight(document.querySelector('#' + id)); + const id = event.target.href.split('#')[1]; + scrollAndHighlightElement(document.querySelector('#' + id)); } if (actionElem === null) return; event.preventDefault(); @@ -132,7 +134,7 @@ class PageComments { this.formContainer.parentNode.style.display = 'block'; this.elem.querySelector('[comment-add-button-container]').style.display = 'none'; this.formInput.focus(); - window.scrollToElement(this.formInput); + this.formInput.scrollIntoView({behavior: "smooth"}); } hideForm() { diff --git a/resources/assets/js/components/page-display.js b/resources/assets/js/components/page-display.js index a3879d006..b2c05ebc6 100644 --- a/resources/assets/js/components/page-display.js +++ b/resources/assets/js/components/page-display.js @@ -1,6 +1,7 @@ import Clipboard from "clipboard/dist/clipboard.min"; import Code from "../services/code"; import * as DOM from "../services/dom"; +import {scrollAndHighlightElement} from "../services/util"; class PageDisplay { @@ -20,10 +21,12 @@ class PageDisplay { // Sidebar page nav click event const sidebarPageNav = document.querySelector('.sidebar-page-nav'); - DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => { - window.components['tri-layout'][0].showContent(); - this.goToText(child.getAttribute('href').substr(1)); - }); + if (sidebarPageNav) { + DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => { + window.components['tri-layout'][0].showContent(); + this.goToText(child.getAttribute('href').substr(1)); + }); + } } goToText(text) { @@ -35,11 +38,11 @@ class PageDisplay { }); if (idElem !== null) { - window.scrollAndHighlight(idElem); + scrollAndHighlightElement(idElem); } else { const textElem = DOM.findText('.page-content > div > *', text); if (textElem) { - window.scrollAndHighlight(textElem); + scrollAndHighlightElement(textElem); } } } diff --git a/resources/assets/js/index.js b/resources/assets/js/index.js index f202c322e..c23615a88 100644 --- a/resources/assets/js/index.js +++ b/resources/assets/js/index.js @@ -1,6 +1,3 @@ -// Global Polyfills -import "./services/dom-polyfills" - // Url retrieval function window.baseUrl = function(path) { let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); @@ -11,27 +8,24 @@ window.baseUrl = function(path) { // Set events and http services on window import Events from "./services/events" -import Http from "./services/http" -let httpInstance = Http(); +import httpInstance from "./services/http" +const eventManager = new Events(); window.$http = httpInstance; -window.$events = new Events(); +window.$events = eventManager; // Translation setup // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system import Translations from "./services/translations" -let translator = new Translations(window.translations); +const translator = new Translations(window.translations); window.trans = translator.get.bind(translator); window.trans_choice = translator.getPlural.bind(translator); -// Load in global UI helpers and libraries including jQuery -import "./services/global-ui" - -// Set services on Vue +// Make services available to Vue instances import Vue from "vue" Vue.prototype.$http = httpInstance; -Vue.prototype.$events = window.$events; +Vue.prototype.$events = eventManager; -// Load vues and components +// Load Vues and components import vues from "./vues/vues" import components from "./components" vues(); diff --git a/resources/assets/js/services/dom-polyfills.js b/resources/assets/js/services/dom-polyfills.js deleted file mode 100644 index d32af9118..000000000 --- a/resources/assets/js/services/dom-polyfills.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Polyfills for DOM API's - */ - -// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches -if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; -} - -// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Browser_compatibility -if (!Element.prototype.closest) { - Element.prototype.closest = function (s) { - var el = this; - var ancestor = this; - if (!document.documentElement.contains(el)) return null; - do { - if (ancestor.matches(s)) return ancestor; - ancestor = ancestor.parentElement; - } while (ancestor !== null); - return null; - }; -} \ No newline at end of file diff --git a/resources/assets/js/services/global-ui.js b/resources/assets/js/services/global-ui.js deleted file mode 100644 index 948e8e880..000000000 --- a/resources/assets/js/services/global-ui.js +++ /dev/null @@ -1,58 +0,0 @@ -// Global jQuery Config & Extensions - -import jQuery from "jquery" -window.jQuery = window.$ = jQuery; - -/** - * Scroll the view to a specific element. - * @param {HTMLElement} element - */ -window.scrollToElement = function(element) { - if (!element) return; - let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; - let top = element.getBoundingClientRect().top + offset; - $('html, body').animate({ - scrollTop: top - 60 // Adjust to change final scroll position top margin - }, 300); -}; - -/** - * Scroll and highlight an element. - * @param {HTMLElement} element - */ -window.scrollAndHighlight = function(element) { - if (!element) return; - window.scrollToElement(element); - let color = document.getElementById('custom-styles').getAttribute('data-color-light'); - let initColor = window.getComputedStyle(element).getPropertyValue('background-color'); - element.style.backgroundColor = color; - setTimeout(() => { - element.classList.add('selectFade'); - element.style.backgroundColor = initColor; - }, 10); - setTimeout(() => { - element.classList.remove('selectFade'); - element.style.backgroundColor = ''; - }, 3000); -}; - -// Smooth scrolling -jQuery.fn.smoothScrollTo = function () { - if (this.length === 0) return; - window.scrollToElement(this[0]); - return this; -}; - -// Making contains text expression not worry about casing -jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) { - return function (elem) { - return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; - }; -}); - -// Detect IE for css -if(navigator.userAgent.indexOf('MSIE')!==-1 - || navigator.appVersion.indexOf('Trident/') > 0 - || navigator.userAgent.indexOf('Safari') !== -1){ - document.body.classList.add('flexbox-support'); -} \ No newline at end of file diff --git a/resources/assets/js/services/http.js b/resources/assets/js/services/http.js index 1e50fe2ae..06cc6a04f 100644 --- a/resources/assets/js/services/http.js +++ b/resources/assets/js/services/http.js @@ -1,21 +1,146 @@ -import axios from "axios" -function instance() { - let axiosInstance = axios.create({ - headers: { - 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), - 'baseURL': window.baseUrl('') - } +/** + * Perform a HTTP GET request. + * Can easily pass query parameters as the second parameter. + * @param {String} url + * @param {Object} params + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function get(url, params = {}) { + return request(url, { + method: 'GET', + params, }); - axiosInstance.interceptors.request.use(resp => { - return resp; - }, err => { - if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err); - if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error); - if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message); - }); - return axiosInstance; } +/** + * Perform a HTTP POST request. + * @param {String} url + * @param {Object} data + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function post(url, data = null) { + return dataRequest('POST', url, data); +} -export default instance; \ No newline at end of file +/** + * Perform a HTTP PUT request. + * @param {String} url + * @param {Object} data + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function put(url, data = null) { + return dataRequest('PUT', url, data); +} + +/** + * Perform a HTTP PATCH request. + * @param {String} url + * @param {Object} data + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function patch(url, data = null) { + return dataRequest('PATCH', url, data); +} + +/** + * Perform a HTTP DELETE request. + * @param {String} url + * @param {Object} data + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function performDelete(url, data = null) { + return dataRequest('DELETE', url, data); +} + +/** + * Perform a HTTP request to the back-end that includes data in the body. + * Parses the body to JSON if an object, setting the correct headers. + * @param {String} method + * @param {String} url + * @param {Object} data + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function dataRequest(method, url, data = null) { + const options = { + method: method, + body: data, + }; + + if (typeof data === 'object') { + options.headers = {'Content-Type': 'application/json'}; + options.body = JSON.stringify(data); + } + + return request(url, options) +} + +/** + * Create a new HTTP request, setting the required CSRF information + * to communicate with the back-end. Parses & formats the response. + * @param {String} url + * @param {Object} options + * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + */ +async function request(url, options = {}) { + if (!url.startsWith('http')) { + url = window.baseUrl(url); + } + + if (options.params) { + const urlObj = new URL(url); + for (let paramName of Object.keys(options.params)) { + const value = options.params[paramName]; + if (typeof value !== 'undefined' && value !== null) { + urlObj.searchParams.set(paramName, value); + } + } + url = urlObj.toString(); + } + + const csrfToken = document.querySelector('meta[name=token]').getAttribute('content'); + options = Object.assign({}, options, { + 'credentials': 'same-origin', + }); + options.headers = Object.assign({}, options.headers || {}, { + 'baseURL': window.baseUrl(''), + 'X-CSRF-TOKEN': csrfToken, + }); + + const response = await fetch(url, options); + const content = await getResponseContent(response); + return { + data: content, + headers: response.headers, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + url: response.url, + original: response, + } +} + +/** + * Get the content from a fetch response. + * Checks the content-type header to determine the format. + * @param response + * @returns {Promise} + */ +async function getResponseContent(response) { + const responseContentType = response.headers.get('Content-Type'); + const subType = responseContentType.split('/').pop(); + + if (subType === 'javascript' || subType === 'json') { + return await response.json(); + } + + return await response.text(); +} + +export default { + get: get, + post: post, + put: put, + patch: patch, + delete: performDelete, +}; \ No newline at end of file diff --git a/resources/assets/js/services/util.js b/resources/assets/js/services/util.js index 727c723c6..b2f291872 100644 --- a/resources/assets/js/services/util.js +++ b/resources/assets/js/services/util.js @@ -24,4 +24,25 @@ export function debounce(func, wait, immediate) { timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; -}; \ No newline at end of file +}; + +/** + * Scroll and highlight an element. + * @param {HTMLElement} element + */ +export function scrollAndHighlightElement(element) { + if (!element) return; + element.scrollIntoView({behavior: 'smooth'}); + + const color = document.getElementById('custom-styles').getAttribute('data-color-light'); + const initColor = window.getComputedStyle(element).getPropertyValue('background-color'); + element.style.backgroundColor = color; + setTimeout(() => { + element.classList.add('selectFade'); + element.style.backgroundColor = initColor; + }, 10); + setTimeout(() => { + element.classList.remove('selectFade'); + element.style.backgroundColor = ''; + }, 3000); +} \ No newline at end of file diff --git a/resources/assets/js/vues/components/autosuggest.js b/resources/assets/js/vues/components/autosuggest.js index 4fe183f02..d76ee89f1 100644 --- a/resources/assets/js/vues/components/autosuggest.js +++ b/resources/assets/js/vues/components/autosuggest.js @@ -113,11 +113,13 @@ const methods = { */ getSuggestions(input, params) { params.search = input; - let cacheKey = `${this.url}:${JSON.stringify(params)}`; + const cacheKey = `${this.url}:${JSON.stringify(params)}`; - if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]); + if (typeof ajaxCache[cacheKey] !== "undefined") { + return Promise.resolve(ajaxCache[cacheKey]); + } - return this.$http.get(this.url, {params}).then(resp => { + return this.$http.get(this.url, params).then(resp => { ajaxCache[cacheKey] = resp.data; return resp.data; }); diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js index dd1d9d17a..6df12d16d 100644 --- a/resources/assets/js/vues/image-manager.js +++ b/resources/assets/js/vues/image-manager.js @@ -57,14 +57,14 @@ const methods = { }, async fetchData() { - let query = { + const params = { page, search: this.searching ? this.searchTerm : null, uploaded_to: this.uploadedTo || null, filter_type: this.filter, }; - const {data} = await this.$http.get(baseUrl, {params: query}); + const {data} = await this.$http.get(baseUrl, params); this.images = this.images.concat(data.images); this.hasMore = data.has_more; page++;