"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",
}
}
},
- "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",
"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",
"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",
"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",
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/)
'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);
import MarkdownIt from "markdown-it";
+import {scrollAndHighlightElement} from "../services/util";
+
const md = new MarkdownIt({ html: false });
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();
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() {
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 {
// 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) {
});
if (idElem !== null) {
- window.scrollAndHighlight(idElem);
+ scrollAndHighlightElement(idElem);
} else {
const textElem = DOM.findText('.page-content > div > *', text);
if (textElem) {
- window.scrollAndHighlight(textElem);
+ scrollAndHighlightElement(textElem);
}
}
}
-// Global Polyfills
-import "./services/dom-polyfills"
-
// Url retrieval function
window.baseUrl = function(path) {
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
// 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();
+++ /dev/null
-/**
- * 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
+++ /dev/null
-// 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
-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,
+ });
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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',
});
- 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);
+ options.headers = Object.assign({}, options.headers || {}, {
+ 'baseURL': window.baseUrl(''),
+ 'X-CSRF-TOKEN': csrfToken,
});
- return axiosInstance;
+
+ 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<Object|String>}
+ */
+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 instance;
\ No newline at end of file
+export default {
+ get: get,
+ post: post,
+ put: put,
+ patch: patch,
+ delete: performDelete,
+};
\ No newline at end of file
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
*/
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;
});
},
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++;