Merge pull request #4181 from BookStackApp/js_formatting

Added standard JS formatting via ESLint
This commit is contained in:
Dan Brown 2023-04-19 23:01:10 +01:00 committed by GitHub
commit 9f467f4052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 3219 additions and 1706 deletions

16
.github/workflows/lint-js.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: lint-js
on: [push, pull_request]
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- name: Install NPM deps
run: npm ci
- name: Run formatting check
run: npm run lint

View File

@ -33,6 +33,10 @@ If the codebase needs to be tested with deprecations, this can be done via uncom
## Code Standards
We use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.
### PHP
PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer).
Static analysis is in place using [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan).
The below commands can be used to utilise these tools:
@ -51,7 +55,19 @@ composer format
composer check-static
```
If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.
### JavaScript
JavaScript code standards use managed using [ESLint](https://eslint.org/).
The ESLint rule configuration is managed within the `package.json` file.
The below commands can be used to lint and format:
```bash
# Run code linting using ESLint
npm run lint
# Fix code where possible using ESLint
npm run fix
```
## Development using Docker

1310
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,17 @@
"dev": "npm-run-all --parallel watch livereload",
"watch": "npm-run-all --parallel build:*:watch",
"livereload": "livereload ./public/dist/",
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
"lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"",
"fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\""
},
"devDependencies": {
"@lezer/generator": "^1.2.2",
"chokidar-cli": "^3.0",
"esbuild": "^0.17.16",
"eslint": "^8.38.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.27.5",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.3.0",
@ -37,6 +42,7 @@
"@codemirror/state": "^6.2.0",
"@codemirror/theme-one-dark": "^6.1.1",
"@codemirror/view": "^6.9.4",
"@lezer/highlight": "^1.1.4",
"@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
"codemirror": "^6.0.1",
@ -45,5 +51,85 @@
"markdown-it-task-lists": "^2.1.1",
"snabbdom": "^3.5.1",
"sortablejs": "^1.15.0"
},
"eslintConfig": {
"root": true,
"env": {
"browser": true,
"es2021": true
},
"extends": "airbnb-base",
"ignorePatterns": [
"resources/**/*-stub.js"
],
"overrides": [],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": [
"error",
4
],
"arrow-parens": [
"error",
"as-needed"
],
"padded-blocks": [
"error",
{
"blocks": "never",
"classes": "always"
}
],
"object-curly-spacing": [
"error",
"never"
],
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"import/prefer-default-export": "off",
"no-plusplus": [
"error",
{
"allowForLoopAfterthoughts": true
}
],
"arrow-body-style": "off",
"no-restricted-syntax": "off",
"no-continue": "off",
"prefer-destructuring": "off",
"class-methods-use-this": "off",
"no-param-reassign": "off",
"no-console": [
"warn",
{
"allow": [
"error",
"warn"
]
}
],
"no-new": "off",
"max-len": [
"error",
{
"code": 110,
"tabWidth": 4,
"ignoreUrls": true,
"ignoreComments": false,
"ignoreRegExpLiterals": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}
]
}
}
}

View File

@ -1,34 +1,37 @@
import * as events from './services/events';
import * as httpInstance from './services/http';
import Translations from './services/translations';
import * as components from './services/components';
import * as componentMap from './components';
// Url retrieval function
window.baseUrl = function(path) {
window.baseUrl = function baseUrl(path) {
let targetPath = path;
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
if (basePath[basePath.length-1] === '/') basePath = basePath.slice(0, basePath.length-1);
if (path[0] === '/') path = path.slice(1);
return basePath + '/' + path;
if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1);
if (targetPath[0] === '/') targetPath = targetPath.slice(1);
return `${basePath}/${targetPath}`;
};
window.importVersioned = function(moduleName) {
window.importVersioned = function importVersioned(moduleName) {
const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
return import(importPath);
};
// Set events and http services on window
import events from "./services/events"
import httpInstance from "./services/http"
window.$http = httpInstance;
window.$events = events;
// 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"
// Creates a global function with name 'trans' to be used in the same way as the Laravel translation system
const translator = new Translations();
window.trans = translator.get.bind(translator);
window.trans_choice = translator.getPlural.bind(translator);
window.trans_plural = translator.parsePlural.bind(translator);
// Load Components
import * as components from "./services/components"
import * as componentMap from "./components";
// Load & initialise components
components.register(componentMap);
window.$components = components;
components.init();

View File

@ -1,29 +1,41 @@
import {EditorView, keymap} from "@codemirror/view";
import {EditorView, keymap} from '@codemirror/view';
import {copyTextToClipboard} from "../services/clipboard.js"
import {viewerExtensions, editorExtensions} from "./setups.js";
import {createView} from "./views.js";
import {SimpleEditorInterface} from "./simple-editor-interface.js";
import {copyTextToClipboard} from '../services/clipboard';
import {viewerExtensions, editorExtensions} from './setups';
import {createView} from './views';
import {SimpleEditorInterface} from './simple-editor-interface';
/**
* Highlight pre elements on a page
* Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
* @param {EditorView} editorView
*/
export function highlight() {
const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
for (const codeBlock of codeBlocks) {
highlightElem(codeBlock);
}
}
function addCopyIcon(editorView) {
const copyIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
const checkIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
const copyButton = document.createElement('button');
copyButton.setAttribute('type', 'button');
copyButton.classList.add('cm-copy-button');
copyButton.innerHTML = copyIcon;
editorView.dom.appendChild(copyButton);
/**
* Highlight all code blocks within the given parent element
* @param {HTMLElement} parent
*/
export function highlightWithin(parent) {
const codeBlocks = parent.querySelectorAll('pre');
for (const codeBlock of codeBlocks) {
highlightElem(codeBlock);
}
const notifyTime = 620;
const transitionTime = 60;
copyButton.addEventListener('click', () => {
copyTextToClipboard(editorView.state.doc.toString());
copyButton.classList.add('success');
setTimeout(() => {
copyButton.innerHTML = checkIcon;
}, transitionTime / 2);
setTimeout(() => {
copyButton.classList.remove('success');
}, notifyTime);
setTimeout(() => {
copyButton.innerHTML = copyIcon;
}, notifyTime + (transitionTime / 2));
});
}
/**
@ -32,7 +44,7 @@ export function highlightWithin(parent) {
*/
function highlightElem(elem) {
const innerCodeElem = elem.querySelector('code[class^=language-]');
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
elem.innerHTML = elem.innerHTML.replace(/<br\s*\/?>/gi, '\n');
const content = elem.textContent.trimEnd();
let langName = '';
@ -57,36 +69,24 @@ function highlightElem(elem) {
}
/**
* Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
* @param {EditorView} editorView
* Highlight all code blocks within the given parent element
* @param {HTMLElement} parent
*/
function addCopyIcon(editorView) {
const copyIcon = `<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
const checkIcon = `<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>`;
const copyButton = document.createElement('button');
copyButton.setAttribute('type', 'button')
copyButton.classList.add('cm-copy-button');
copyButton.innerHTML = copyIcon;
editorView.dom.appendChild(copyButton);
export function highlightWithin(parent) {
const codeBlocks = parent.querySelectorAll('pre');
for (const codeBlock of codeBlocks) {
highlightElem(codeBlock);
}
}
const notifyTime = 620;
const transitionTime = 60;
copyButton.addEventListener('click', event => {
copyTextToClipboard(editorView.state.doc.toString());
copyButton.classList.add('success');
setTimeout(() => {
copyButton.innerHTML = checkIcon;
}, transitionTime / 2);
setTimeout(() => {
copyButton.classList.remove('success');
}, notifyTime);
setTimeout(() => {
copyButton.innerHTML = copyIcon;
}, notifyTime + (transitionTime / 2));
});
/**
* Highlight pre elements on a page
*/
export function highlight() {
const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
for (const codeBlock of codeBlocks) {
highlightElem(codeBlock);
}
}
/**
@ -112,7 +112,6 @@ export function wysiwygView(cmContainer, shadowRoot, content, language) {
return editor;
}
/**
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
* @param {HTMLElement} elem
@ -126,7 +125,7 @@ export function popupEditor(elem, modeSuggestion) {
doc: content,
extensions: [
...editorExtensions(elem.parentElement),
EditorView.updateListener.of((v) => {
EditorView.updateListener.of(v => {
if (v.docChanged) {
// textArea.value = v.state.doc.toString();
}
@ -155,7 +154,7 @@ export function inlineEditor(textArea, mode) {
doc: content,
extensions: [
...editorExtensions(textArea.parentElement),
EditorView.updateListener.of((v) => {
EditorView.updateListener.of(v => {
if (v.docChanged) {
textArea.value = v.state.doc.toString();
}
@ -188,7 +187,7 @@ export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) {
extensions: [
keymap.of(keyBindings),
...editorExtensions(elem.parentElement),
EditorView.updateListener.of((v) => {
EditorView.updateListener.of(v => {
onChange(v);
}),
EditorView.domEventHandlers(domEventHandlers),
@ -204,4 +203,4 @@ export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) {
elem.style.display = 'none';
return ev;
}
}

View File

@ -1,20 +1,19 @@
import {StreamLanguage} from "@codemirror/language"
import {StreamLanguage} from '@codemirror/language';
import {css} from '@codemirror/lang-css';
import {json} from '@codemirror/lang-json';
import {javascript} from '@codemirror/lang-javascript';
import {html} from "@codemirror/lang-html";
import {html} from '@codemirror/lang-html';
import {markdown} from '@codemirror/lang-markdown';
import {php} from '@codemirror/lang-php';
import {twig} from "@ssddanbrown/codemirror-lang-twig";
import {xml} from "@codemirror/lang-xml";
import {twig} from '@ssddanbrown/codemirror-lang-twig';
import {xml} from '@codemirror/lang-xml';
const legacyLoad = async (mode) => {
const legacyLoad = async mode => {
const modes = await window.importVersioned('legacy-modes');
return StreamLanguage.define(modes[mode]);
};
// Mapping of possible languages or formats from user input to their codemirror modes.
// Value can be a mode string or a function that will receive the code content & return the mode string.
// The function option is used in the event the exact mode could be dynamic depending on the code.
@ -58,7 +57,7 @@ const modeMap = {
pascal: () => legacyLoad('pascal'),
perl: () => legacyLoad('perl'),
pgsql: () => legacyLoad('pgSQL'),
php: async (code) => {
php: async code => {
const hasTags = code.includes('<?php');
return php({plain: !hasTags});
},
@ -113,4 +112,4 @@ export function getLanguageExtension(langSuggestion, content) {
}
return language(content);
}
}

View File

@ -1,4 +1,6 @@
export {c, cpp, csharp, java, kotlin, scala, dart} from '@codemirror/legacy-modes/mode/clike';
export {
c, cpp, csharp, java, kotlin, scala, dart,
} from '@codemirror/legacy-modes/mode/clike';
export {diff} from '@codemirror/legacy-modes/mode/diff';
export {fortran} from '@codemirror/legacy-modes/mode/fortran';
export {go} from '@codemirror/legacy-modes/mode/go';
@ -17,11 +19,13 @@ export {ruby} from '@codemirror/legacy-modes/mode/ruby';
export {rust} from '@codemirror/legacy-modes/mode/rust';
export {scheme} from '@codemirror/legacy-modes/mode/scheme';
export {shell} from '@codemirror/legacy-modes/mode/shell';
export {standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL} from '@codemirror/legacy-modes/mode/sql';
export {
standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL,
} from '@codemirror/legacy-modes/mode/sql';
export {stex} from '@codemirror/legacy-modes/mode/stex';
export {swift} from "@codemirror/legacy-modes/mode/swift";
export {swift} from '@codemirror/legacy-modes/mode/swift';
export {toml} from '@codemirror/legacy-modes/mode/toml';
export {vb} from '@codemirror/legacy-modes/mode/vb';
export {vbScript} from '@codemirror/legacy-modes/mode/vbscript';
export {yaml} from '@codemirror/legacy-modes/mode/yaml';
export {smarty} from "@ssddanbrown/codemirror-lang-smarty";
export {smarty} from '@ssddanbrown/codemirror-lang-smarty';

View File

@ -1,9 +1,13 @@
import {EditorView, keymap, drawSelection, highlightActiveLine, dropCursor,
rectangularSelection, lineNumbers, highlightActiveLineGutter} from "@codemirror/view"
import {bracketMatching} from "@codemirror/language"
import {defaultKeymap, history, historyKeymap, indentWithTab} from "@codemirror/commands"
import {EditorState} from "@codemirror/state"
import {getTheme} from "./themes";
import {
EditorView, keymap, drawSelection, highlightActiveLine, dropCursor,
rectangularSelection, lineNumbers, highlightActiveLineGutter,
} from '@codemirror/view';
import {bracketMatching} from '@codemirror/language';
import {
defaultKeymap, history, historyKeymap, indentWithTab,
} from '@codemirror/commands';
import {EditorState} from '@codemirror/state';
import {getTheme} from './themes';
/**
* @param {Element} parentEl
@ -51,4 +55,4 @@ export function editorExtensions(parentEl) {
]),
EditorView.lineWrapping,
];
}
}

View File

@ -1,7 +1,7 @@
import {updateViewLanguage} from "./views";
import {updateViewLanguage} from './views';
export class SimpleEditorInterface {
/**
* @param {EditorView} editorView
*/
@ -22,9 +22,9 @@ export class SimpleEditorInterface {
* @param content
*/
setContent(content) {
const doc = this.ev.state.doc;
const {doc} = this.ev.state;
this.ev.dispatch({
changes: {from: 0, to: doc.length, insert: content}
changes: {from: 0, to: doc.length, insert: content},
});
}
@ -43,4 +43,5 @@ export class SimpleEditorInterface {
setMode(mode, content = '') {
updateViewLanguage(this.ev, mode, content);
}
}
}

View File

@ -1,62 +1,102 @@
import {tags} from "@lezer/highlight";
import {HighlightStyle, syntaxHighlighting} from "@codemirror/language";
import {EditorView} from "@codemirror/view";
import {oneDarkHighlightStyle, oneDarkTheme} from "@codemirror/theme-one-dark";
import {tags} from '@lezer/highlight';
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
import {EditorView} from '@codemirror/view';
import {oneDarkHighlightStyle, oneDarkTheme} from '@codemirror/theme-one-dark';
const defaultLightHighlightStyle = HighlightStyle.define([
{ tag: tags.meta,
color: "#388938" },
{ tag: tags.link,
textDecoration: "underline" },
{ tag: tags.heading,
textDecoration: "underline",
fontWeight: "bold" },
{ tag: tags.emphasis,
fontStyle: "italic" },
{ tag: tags.strong,
fontWeight: "bold" },
{ tag: tags.strikethrough,
textDecoration: "line-through" },
{ tag: tags.keyword,
color: "#708" },
{ tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName],
color: "#219" },
{ tag: [tags.literal, tags.inserted],
color: "#164" },
{ tag: [tags.string, tags.deleted],
color: "#a11" },
{ tag: [tags.regexp, tags.escape, tags.special(tags.string)],
color: "#e40" },
{ tag: tags.definition(tags.variableName),
color: "#00f" },
{ tag: tags.local(tags.variableName),
color: "#30a" },
{ tag: [tags.typeName, tags.namespace],
color: "#085" },
{ tag: tags.className,
color: "#167" },
{ tag: [tags.special(tags.variableName), tags.macroName],
color: "#256" },
{ tag: tags.definition(tags.propertyName),
color: "#00c" },
{ tag: tags.compareOperator,
color: "#708" },
{ tag: tags.comment,
color: "#940" },
{ tag: tags.invalid,
color: "#f00" }
{
tag: tags.meta,
color: '#388938',
},
{
tag: tags.link,
textDecoration: 'underline',
},
{
tag: tags.heading,
textDecoration: 'underline',
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.strikethrough,
textDecoration: 'line-through',
},
{
tag: tags.keyword,
color: '#708',
},
{
tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName],
color: '#219',
},
{
tag: [tags.literal, tags.inserted],
color: '#164',
},
{
tag: [tags.string, tags.deleted],
color: '#a11',
},
{
tag: [tags.regexp, tags.escape, tags.special(tags.string)],
color: '#e40',
},
{
tag: tags.definition(tags.variableName),
color: '#00f',
},
{
tag: tags.local(tags.variableName),
color: '#30a',
},
{
tag: [tags.typeName, tags.namespace],
color: '#085',
},
{
tag: tags.className,
color: '#167',
},
{
tag: [tags.special(tags.variableName), tags.macroName],
color: '#256',
},
{
tag: tags.definition(tags.propertyName),
color: '#00c',
},
{
tag: tags.compareOperator,
color: '#708',
},
{
tag: tags.comment,
color: '#940',
},
{
tag: tags.invalid,
color: '#f00',
},
]);
const defaultThemeSpec = {
"&": {
backgroundColor: "#FFF",
color: "#000",
'&': {
backgroundColor: '#FFF',
color: '#000',
},
"&.cm-focused": {
outline: "none",
'&.cm-focused': {
outline: 'none',
},
".cm-line": {
lineHeight: "1.6",
'.cm-line': {
lineHeight: '1.6',
},
};
@ -82,10 +122,10 @@ export function getTheme(viewParentEl) {
if (tagStyles.length) {
highlightStyle = HighlightStyle.define(tagStyles);
}
}
},
};
window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData);
return [viewTheme, syntaxHighlighting(highlightStyle)];
}
}

View File

@ -1,6 +1,6 @@
import {Compartment} from "@codemirror/state";
import {EditorView} from "@codemirror/view";
import {getLanguageExtension} from "./languages";
import {Compartment} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {getLanguageExtension} from './languages';
const viewLangCompartments = new WeakMap();
@ -33,6 +33,6 @@ export async function updateViewLanguage(ev, modeSuggestion, content) {
const language = await getLanguageExtension(modeSuggestion, content);
ev.dispatch({
effects: compartment.reconfigure(language ? language : [])
effects: compartment.reconfigure(language || []),
});
}
}

View File

@ -1,6 +1,6 @@
import {onChildEvent} from "../services/dom";
import {uniqueId} from "../services/util";
import {Component} from "./component";
import {onChildEvent} from '../services/dom';
import {uniqueId} from '../services/util';
import {Component} from './component';
/**
* AddRemoveRows
@ -8,6 +8,7 @@ import {Component} from "./component";
* Needs a model row to use when adding a new row.
*/
export class AddRemoveRows extends Component {
setup() {
this.modelRow = this.$refs.model;
this.addButton = this.$refs.add;
@ -19,7 +20,7 @@ export class AddRemoveRows extends Component {
setupListeners() {
this.addButton.addEventListener('click', this.add.bind(this));
onChildEvent(this.$el, this.removeSelector, 'click', (e) => {
onChildEvent(this.$el, this.removeSelector, 'click', e => {
const row = e.target.closest(this.rowSelector);
row.remove();
});
@ -44,9 +45,10 @@ export class AddRemoveRows extends Component {
*/
setClonedInputNames(clone) {
const rowId = uniqueId();
const randRowIdElems = clone.querySelectorAll(`[name*="randrowid"]`);
const randRowIdElems = clone.querySelectorAll('[name*="randrowid"]');
for (const elem of randRowIdElems) {
elem.name = elem.name.split('randrowid').join(rowId);
}
}
}
}

View File

@ -1,7 +1,8 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {Component} from './component';
export class AjaxDeleteRow extends Component {
setup() {
this.row = this.$el;
this.url = this.$opts.url;
@ -19,9 +20,10 @@ export class AjaxDeleteRow extends Component {
window.$events.emit('success', resp.data.message);
}
this.row.remove();
}).catch(err => {
}).catch(() => {
this.row.style.opacity = null;
this.row.style.pointerEvents = null;
});
}
}
}

View File

@ -1,5 +1,5 @@
import {onEnterPress, onSelect} from "../services/dom";
import {Component} from "./component";
import {onEnterPress, onSelect} from '../services/dom';
import {Component} from './component';
/**
* Ajax Form
@ -11,6 +11,7 @@ import {Component} from "./component";
* otherwise will act as a fake form element.
*/
export class AjaxForm extends Component {
setup() {
this.container = this.$el;
this.responseContainer = this.container;
@ -27,7 +28,6 @@ export class AjaxForm extends Component {
}
setupListeners() {
if (this.container.tagName === 'FORM') {
this.container.addEventListener('submit', this.submitRealForm.bind(this));
return;
@ -43,7 +43,7 @@ export class AjaxForm extends Component {
submitFakeForm() {
const fd = new FormData();
const inputs = this.container.querySelectorAll(`[name]`);
const inputs = this.container.querySelectorAll('[name]');
for (const input of inputs) {
fd.append(input.getAttribute('name'), input.value);
}
@ -76,4 +76,4 @@ export class AjaxForm extends Component {
this.responseContainer.style.pointerEvents = null;
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
/**
* Attachments List
@ -13,11 +13,11 @@ export class AttachmentsList extends Component {
}
setupListeners() {
const isExpectedKey = (event) => event.key === 'Control' || event.key === 'Meta';
const isExpectedKey = event => event.key === 'Control' || event.key === 'Meta';
window.addEventListener('keydown', event => {
if (isExpectedKey(event)) {
if (isExpectedKey(event)) {
this.addOpenQueryToLinks();
}
}
}, {passive: true});
window.addEventListener('keyup', event => {
if (isExpectedKey(event)) {
@ -30,7 +30,7 @@ export class AttachmentsList extends Component {
const links = this.container.querySelectorAll('a.attachment-file');
for (const link of links) {
if (link.href.split('?')[1] !== 'open=true') {
link.href = link.href + '?open=true';
link.href += '?open=true';
link.setAttribute('target', '_blank');
}
}
@ -43,4 +43,5 @@ export class AttachmentsList extends Component {
link.removeAttribute('target');
}
}
}
}

View File

@ -1,5 +1,5 @@
import {showLoading} from "../services/dom";
import {Component} from "./component";
import {showLoading} from '../services/dom';
import {Component} from './component';
export class Attachments extends Component {
@ -27,7 +27,7 @@ export class Attachments extends Component {
this.startEdit(event.detail.id);
});
this.container.addEventListener('event-emit-select-edit-back', event => {
this.container.addEventListener('event-emit-select-edit-back', () => {
this.stopEdit();
});
@ -73,4 +73,4 @@ export class Attachments extends Component {
this.listContainer.classList.remove('hidden');
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class AutoSubmit extends Component {
@ -8,4 +8,4 @@ export class AutoSubmit extends Component {
this.form.submit();
}
}
}

View File

@ -1,7 +1,7 @@
import {escapeHtml} from "../services/util";
import {onChildEvent} from "../services/dom";
import {Component} from "./component";
import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
import {escapeHtml} from '../services/util';
import {onChildEvent} from '../services/dom';
import {Component} from './component';
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
const ajaxCache = {};
@ -9,6 +9,7 @@ const ajaxCache = {};
* AutoSuggest
*/
export class AutoSuggest extends Component {
setup() {
this.parent = this.$el.parentElement;
this.container = this.$el;
@ -24,7 +25,7 @@ export class AutoSuggest extends Component {
setupListeners() {
const navHandler = new KeyboardNavigationHandler(
this.list,
event => {
() => {
this.input.focus();
setTimeout(() => this.hideSuggestions(), 1);
},
@ -67,9 +68,7 @@ export class AutoSuggest extends Component {
const search = this.input.value.toLowerCase();
const suggestions = await this.loadSuggestions(search, nameFilter);
const toShow = suggestions.filter(val => {
return search === '' || val.toLowerCase().startsWith(search);
}).slice(0, 10);
const toShow = suggestions.filter(val => search === '' || val.toLowerCase().startsWith(search)).slice(0, 10);
this.displaySuggestions(toShow);
}
@ -105,7 +104,8 @@ export class AutoSuggest extends Component {
*/
displaySuggestions(suggestions) {
if (suggestions.length === 0) {
return this.hideSuggestions();
this.hideSuggestions();
return;
}
// This used to use <button>s but was changed to div elements since Safari would not focus on buttons
@ -126,4 +126,5 @@ export class AutoSuggest extends Component {
this.hideSuggestions();
}
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class BackToTop extends Component {
@ -18,7 +18,7 @@ export class BackToTop extends Component {
}
onPageScroll() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
const scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!this.showing && scrollTopPos > this.breakPoint) {
this.button.style.display = 'block';
this.showing = true;
@ -35,15 +35,15 @@ export class BackToTop extends Component {
}
scrollToTop() {
let targetTop = this.targetElem.getBoundingClientRect().top;
let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
let duration = 300;
let start = Date.now();
let scrollStart = this.targetElem.getBoundingClientRect().top;
const targetTop = this.targetElem.getBoundingClientRect().top;
const scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
const duration = 300;
const start = Date.now();
const scrollStart = this.targetElem.getBoundingClientRect().top;
function setPos() {
let percentComplete = (1-((Date.now() - start) / duration));
let target = Math.abs(percentComplete * scrollStart);
const percentComplete = (1 - ((Date.now() - start) / duration));
const target = Math.abs(percentComplete * scrollStart);
if (percentComplete > 0) {
scrollElem.scrollTop = target;
requestAnimationFrame(setPos.bind(this));
@ -55,4 +55,4 @@ export class BackToTop extends Component {
requestAnimationFrame(setPos.bind(this));
}
}
}

View File

@ -1,25 +1,25 @@
import Sortable, {MultiDrag} from "sortablejs";
import {Component} from "./component";
import {htmlToDom} from "../services/dom";
import Sortable, {MultiDrag} from 'sortablejs';
import {Component} from './component';
import {htmlToDom} from '../services/dom';
// Auto sort control
const sortOperations = {
name: function(a, b) {
name(a, b) {
const aName = a.getAttribute('data-name').trim().toLowerCase();
const bName = b.getAttribute('data-name').trim().toLowerCase();
return aName.localeCompare(bName);
},
created: function(a, b) {
created(a, b) {
const aTime = Number(a.getAttribute('data-created'));
const bTime = Number(b.getAttribute('data-created'));
return bTime - aTime;
},
updated: function(a, b) {
updated(a, b) {
const aTime = Number(a.getAttribute('data-updated'));
const bTime = Number(b.getAttribute('data-updated'));
return bTime - aTime;
},
chaptersFirst: function(a, b) {
chaptersFirst(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
@ -27,7 +27,7 @@ const sortOperations = {
}
return (aType === 'chapter' ? -1 : 1);
},
chaptersLast: function(a, b) {
chaptersLast(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
@ -45,22 +45,22 @@ const sortOperations = {
*/
const moveActions = {
up: {
active(elem, parent, book) {
active(elem, parent) {
return !(elem.previousElementSibling === null && !parent);
},
run(elem, parent, book) {
run(elem, parent) {
const newSibling = elem.previousElementSibling || parent;
newSibling.insertAdjacentElement('beforebegin', elem);
}
},
},
down: {
active(elem, parent, book) {
active(elem, parent) {
return !(elem.nextElementSibling === null && !parent);
},
run(elem, parent, book) {
run(elem, parent) {
const newSibling = elem.nextElementSibling || parent;
newSibling.insertAdjacentElement('afterend', elem);
}
},
},
next_book: {
active(elem, parent, book) {
@ -69,7 +69,7 @@ const moveActions = {
run(elem, parent, book) {
const newList = book.nextElementSibling.querySelector('ul');
newList.prepend(elem);
}
},
},
prev_book: {
active(elem, parent, book) {
@ -78,13 +78,13 @@ const moveActions = {
run(elem, parent, book) {
const newList = book.previousElementSibling.querySelector('ul');
newList.appendChild(elem);
}
},
},
next_chapter: {
active(elem, parent, book) {
active(elem, parent) {
return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
},
run(elem, parent, book) {
run(elem, parent) {
const nextChapter = this.getNextChapter(elem, parent);
nextChapter.querySelector('ul').prepend(elem);
},
@ -92,14 +92,14 @@ const moveActions = {
const topLevel = (parent || elem);
const topItems = Array.from(topLevel.parentElement.children);
const index = topItems.indexOf(topLevel);
return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
}
return topItems.slice(index + 1).find(item => item.dataset.type === 'chapter');
},
},
prev_chapter: {
active(elem, parent, book) {
active(elem, parent) {
return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
},
run(elem, parent, book) {
run(elem, parent) {
const prevChapter = this.getPrevChapter(elem, parent);
prevChapter.querySelector('ul').append(elem);
},
@ -107,40 +107,40 @@ const moveActions = {
const topLevel = (parent || elem);
const topItems = Array.from(topLevel.parentElement.children);
const index = topItems.indexOf(topLevel);
return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
}
return topItems.slice(0, index).reverse().find(item => item.dataset.type === 'chapter');
},
},
book_end: {
active(elem, parent, book) {
active(elem, parent) {
return parent || (parent === null && elem.nextElementSibling);
},
run(elem, parent, book) {
book.querySelector('ul').append(elem);
}
},
},
book_start: {
active(elem, parent, book) {
active(elem, parent) {
return parent || (parent === null && elem.previousElementSibling);
},
run(elem, parent, book) {
book.querySelector('ul').prepend(elem);
}
},
},
before_chapter: {
active(elem, parent, book) {
active(elem, parent) {
return parent;
},
run(elem, parent, book) {
run(elem, parent) {
parent.insertAdjacentElement('beforebegin', elem);
}
},
},
after_chapter: {
active(elem, parent, book) {
active(elem, parent) {
return parent;
},
run(elem, parent, book) {
run(elem, parent) {
parent.insertAdjacentElement('afterend', elem);
}
},
},
};
@ -196,12 +196,12 @@ export class BookSort extends Component {
reverse = (lastSort === sort) ? !reverse : false;
let sortFunction = sortOperations[sort];
if (reverse && reversibleTypes.includes(sort)) {
sortFunction = function(a, b) {
return 0 - sortOperations[sort](a, b)
sortFunction = function reverseSortOperation(a, b) {
return 0 - sortOperations[sort](a, b);
};
}
for (let list of sortLists) {
for (const list of sortLists) {
const directItems = Array.from(list.children).filter(child => child.matches('li'));
directItems.sort(sortFunction).forEach(sortedItem => {
list.appendChild(sortedItem);
@ -221,7 +221,7 @@ export class BookSort extends Component {
const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
if (alreadyAdded) return;
const entitySortItemUrl = entityInfo.link + '/sort-item';
const entitySortItemUrl = `${entityInfo.link}/sort-item`;
window.$http.get(entitySortItemUrl).then(resp => {
const newBookContainer = htmlToDom(resp.data);
this.sortContainer.append(newBookContainer);
@ -249,9 +249,9 @@ export class BookSort extends Component {
const chapterGroupConfig = {
name: 'chapter',
pull: ['book', 'chapter'],
put: function(toList, fromList, draggedElem) {
put(toList, fromList, draggedElem) {
return draggedElem.getAttribute('data-type') === 'page';
}
},
};
for (const sortElem of sortElems) {
@ -260,8 +260,8 @@ export class BookSort extends Component {
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onSort: (event) => {
this.ensureNoNestedChapters()
onSort: () => {
this.ensureNoNestedChapters();
this.updateMapInput();
this.updateMoveActionStateForAll();
},
@ -304,7 +304,7 @@ export class BookSort extends Component {
const entityMap = [];
const lists = this.container.querySelectorAll('.sort-list');
for (let list of lists) {
for (const list of lists) {
const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
const directChildren = Array.from(list.children)
.filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
@ -332,9 +332,9 @@ export class BookSort extends Component {
entityMap.push({
id: childId,
sort: index,
parentChapter: parentChapter,
type: type,
book: bookId
parentChapter,
type,
book: bookId,
});
const subPages = childElem.querySelectorAll('[data-type="page"]');
@ -344,7 +344,7 @@ export class BookSort extends Component {
sort: i,
parentChapter: childId,
type: 'page',
book: bookId
book: bookId,
});
}
}
@ -383,4 +383,5 @@ export class BookSort extends Component {
this.updateMoveActionState(item);
}
}
}
}

View File

@ -1,5 +1,5 @@
import {slideUp, slideDown} from "../services/animations";
import {Component} from "./component";
import {slideUp, slideDown} from '../services/animations';
import {Component} from './component';
export class ChapterContents extends Component {
@ -27,6 +27,11 @@ export class ChapterContents extends Component {
click(event) {
event.preventDefault();
this.isOpen ? this.close() : this.open();
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}

View File

@ -1,6 +1,5 @@
import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
import {Component} from "./component";
import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
import {Component} from './component';
export class CodeEditor extends Component {
@ -10,7 +9,9 @@ export class CodeEditor extends Component {
editor = null;
callback = null;
history = {};
historyKey = 'code_history';
setup() {
@ -42,9 +43,9 @@ export class CodeEditor extends Component {
this.languageInputChange(language);
});
onEnterPress(this.languageInput, e => this.save());
this.languageInput.addEventListener('input', e => this.languageInputChange(this.languageInput.value));
onSelect(this.saveButton, e => this.save());
onEnterPress(this.languageInput, () => this.save());
this.languageInput.addEventListener('input', () => this.languageInputChange(this.languageInput.value));
onSelect(this.saveButton, () => this.save());
onChildEvent(this.historyList, 'button', 'click', (event, elem) => {
event.preventDefault();
@ -73,17 +74,18 @@ export class CodeEditor extends Component {
onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {
isFavorite = !isFavorite;
isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
const action = isFavorite ? this.favourites.add : this.favourites.delete;
action(language);
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
window.$http.patch('/preferences/update-code-language-favourite', {
language: language,
active: isFavorite
language,
active: isFavorite,
});
this.sortLanguageList();
if (isFavorite) {
button.scrollIntoView({block: "center", behavior: "smooth"});
button.scrollIntoView({block: 'center', behavior: 'smooth'});
}
});
}
@ -95,7 +97,7 @@ export class CodeEditor extends Component {
if (aFav && !bFav) {
return -1;
} else if (bFav && !aFav) {
} if (bFav && !aFav) {
return 1;
}
@ -133,7 +135,7 @@ export class CodeEditor extends Component {
this.getPopup().show(() => {
this.editor.focus();
}, () => {
this.addHistory()
this.addHistory();
});
}
@ -162,7 +164,7 @@ export class CodeEditor extends Component {
const isMatch = inputLang === lang;
link.classList.toggle('active', isMatch);
if (isMatch) {
link.scrollIntoView({block: "center", behavior: "smooth"});
link.scrollIntoView({block: 'center', behavior: 'smooth'});
}
}
}
@ -172,8 +174,8 @@ export class CodeEditor extends Component {
const historyKeys = Object.keys(this.history).reverse();
this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
this.historyList.innerHTML = historyKeys.map(key => {
const localTime = (new Date(parseInt(key))).toLocaleTimeString();
return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
const localTime = (new Date(parseInt(key, 10))).toLocaleTimeString();
return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
}).join('');
}
@ -191,4 +193,4 @@ export class CodeEditor extends Component {
window.sessionStorage.setItem(this.historyKey, historyString);
}
}
}

View File

@ -1,6 +1,6 @@
import {Component} from "./component";
import {Component} from './component';
export class CodeHighlighter extends Component{
export class CodeHighlighter extends Component {
setup() {
const container = this.$el;
@ -8,9 +8,9 @@ export class CodeHighlighter extends Component{
const codeBlocks = container.querySelectorAll('pre');
if (codeBlocks.length > 0) {
window.importVersioned('code').then(Code => {
Code.highlightWithin(container);
Code.highlightWithin(container);
});
}
}
}
}

View File

@ -2,14 +2,14 @@
* A simple component to render a code editor within the textarea
* this exists upon.
*/
import {Component} from "./component";
import {Component} from './component';
export class CodeTextarea extends Component {
async setup() {
const mode = this.$opts.mode;
const {mode} = this.$opts;
const Code = await window.importVersioned('code');
Code.inlineEditor(this.$el, mode);
}
}
}

View File

@ -1,5 +1,5 @@
import {slideDown, slideUp} from "../services/animations";
import {Component} from "./component";
import {slideDown, slideUp} from '../services/animations';
import {Component} from './component';
/**
* Collapsible
@ -45,4 +45,4 @@ export class Collapsible extends Component {
}
}
}
}

View File

@ -51,8 +51,9 @@ export class Component {
const componentName = this.$name;
const event = new CustomEvent(`${componentName}-${eventName}`, {
bubbles: true,
detail: data
detail: data,
});
this.$el.dispatchEvent(event);
}
}
}

View File

@ -1,5 +1,5 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {Component} from './component';
/**
* Custom equivalent of window.confirm() using our popup component.
@ -25,8 +25,8 @@ export class ConfirmDialog extends Component {
this.sendResult(false);
});
return new Promise((res, rej) => {
this.res = res;
return new Promise(res => {
this.res = res;
});
}
@ -42,9 +42,9 @@ export class ConfirmDialog extends Component {
*/
sendResult(result) {
if (this.res) {
this.res(result)
this.res(result);
this.res = null;
}
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class CustomCheckbox extends Component {
@ -30,4 +30,4 @@ export class CustomCheckbox extends Component {
this.display.setAttribute('aria-checked', checked);
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class DetailsHighlighter extends Component {
@ -19,4 +19,5 @@ export class DetailsHighlighter extends Component {
}
this.dealtWith = true;
}
}
}

View File

@ -1,6 +1,6 @@
import {debounce} from "../services/util";
import {transitionHeight} from "../services/animations";
import {Component} from "./component";
import {debounce} from '../services/util';
import {transitionHeight} from '../services/animations';
import {Component} from './component';
export class DropdownSearch extends Component {
@ -40,7 +40,7 @@ export class DropdownSearch extends Component {
runLocalSearch(searchTerm) {
const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
for (let listItem of listItems) {
for (const listItem of listItems) {
const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
listItem.style.display = match ? 'flex' : 'none';
listItem.classList.toggle('hidden', !match);
@ -79,4 +79,4 @@ export class DropdownSearch extends Component {
this.loadingElem.style.display = show ? 'block' : 'none';
}
}
}

View File

@ -1,6 +1,6 @@
import {onSelect} from "../services/dom";
import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
import {Component} from './component';
/**
* Dropdown
@ -41,7 +41,11 @@ export class Dropdown extends Component {
this.menu.style.position = 'fixed';
this.menu.style.width = `${menuOriginalRect.width}px`;
this.menu.style.left = `${menuOriginalRect.left}px`;
heightOffset = dropUpwards ? (window.innerHeight - menuOriginalRect.top - toggleHeight / 2) : menuOriginalRect.top;
if (dropUpwards) {
heightOffset = (window.innerHeight - menuOriginalRect.top - toggleHeight / 2);
} else {
heightOffset = menuOriginalRect.top;
}
}
// Adjust menu to display upwards if near the bottom of the screen
@ -55,8 +59,8 @@ export class Dropdown extends Component {
// Set listener to hide on mouse leave or window click
this.menu.addEventListener('mouseleave', this.hide);
window.addEventListener('click', event => {
if (!this.menu.contains(event.target)) {
window.addEventListener('click', clickEvent => {
if (!this.menu.contains(clickEvent.target)) {
this.hide();
}
});
@ -76,7 +80,7 @@ export class Dropdown extends Component {
}
hideAll() {
for (let dropdown of window.$components.get('dropdown')) {
for (const dropdown of window.$components.get('dropdown')) {
dropdown.hide();
}
}
@ -100,13 +104,13 @@ export class Dropdown extends Component {
}
setupListeners() {
const keyboardNavHandler = new KeyboardNavigationHandler(this.container, (event) => {
const keyboardNavHandler = new KeyboardNavigationHandler(this.container, event => {
this.hide();
this.toggle.focus();
if (!this.bubbleEscapes) {
event.stopPropagation();
}
}, (event) => {
}, event => {
if (event.target.nodeName === 'INPUT') {
event.preventDefault();
event.stopPropagation();
@ -120,10 +124,10 @@ export class Dropdown extends Component {
// Hide menu on option click
this.container.addEventListener('click', event => {
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.includes(event.target)) {
this.hide();
}
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.includes(event.target)) {
this.hide();
}
});
onSelect(this.toggle, event => {

View File

@ -1,8 +1,9 @@
import DropZoneLib from "dropzone";
import {fadeOut} from "../services/animations";
import {Component} from "./component";
import DropZoneLib from 'dropzone';
import {fadeOut} from '../services/animations';
import {Component} from './component';
export class Dropzone extends Component {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
@ -12,7 +13,7 @@ export class Dropzone extends Component {
this.uploadLimitMessage = this.$opts.uploadLimitMessage;
this.timeoutMessage = this.$opts.timeoutMessage;
const _this = this;
const component = this;
this.dz = new DropZoneLib(this.container, {
addRemoveLinks: true,
dictRemoveFile: this.removeMessage,
@ -22,22 +23,21 @@ export class Dropzone extends Component {
withCredentials: true,
init() {
this.dz = this;
this.dz.on('sending', _this.onSending.bind(_this));
this.dz.on('success', _this.onSuccess.bind(_this));
this.dz.on('error', _this.onError.bind(_this));
}
this.dz.on('sending', component.onSending.bind(component));
this.dz.on('success', component.onSuccess.bind(component));
this.dz.on('error', component.onError.bind(component));
},
});
}
onSending(file, xhr, data) {
const token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
xhr.ontimeout = (e) => {
xhr.ontimeout = () => {
this.dz.emit('complete', file);
this.dz.emit('error', file, this.timeoutMessage);
}
};
}
onSuccess(file, data) {
@ -55,10 +55,10 @@ export class Dropzone extends Component {
onError(file, errorMessage, xhr) {
this.$emit('error', {file, errorMessage, xhr});
const setMessage = (message) => {
const setMessage = message => {
const messsageEl = file.previewElement.querySelector('[data-dz-errormessage]');
messsageEl.textContent = message;
}
};
if (xhr && xhr.status === 413) {
setMessage(this.uploadLimitMessage);
@ -70,4 +70,5 @@ export class Dropzone extends Component {
removeAll() {
this.dz.removeAllFiles(true);
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class EditorToolbox extends Component {
@ -35,11 +35,10 @@ export class EditorToolbox extends Component {
}
setActiveTab(tabName, openToolbox = false) {
// Set button visibility
for (const button of this.buttons) {
button.classList.remove('active');
const bName = button.dataset.tab;
const bName = button.dataset.tab;
if (bName === tabName) button.classList.add('active');
}
@ -55,4 +54,4 @@ export class EditorToolbox extends Component {
}
}
}
}

View File

@ -1,5 +1,5 @@
import {htmlToDom} from "../services/dom";
import {Component} from "./component";
import {htmlToDom} from '../services/dom';
import {Component} from './component';
export class EntityPermissions extends Component {
@ -29,12 +29,12 @@ export class EntityPermissions extends Component {
this.container.addEventListener('click', event => {
const button = event.target.closest('button');
if (button && button.dataset.roleId) {
this.removeRowOnButtonClick(button)
this.removeRowOnButtonClick(button);
}
});
// Role select change
this.roleSelect.addEventListener('change', event => {
this.roleSelect.addEventListener('change', () => {
const roleId = this.roleSelect.value;
if (roleId) {
this.addRoleRow(roleId);
@ -61,8 +61,8 @@ export class EntityPermissions extends Component {
removeRowOnButtonClick(button) {
const row = button.closest('.item-list-row');
const roleId = button.dataset.roleId;
const roleName = button.dataset.roleName;
const {roleId} = button.dataset;
const {roleName} = button.dataset;
const option = document.createElement('option');
option.value = roleId;
@ -72,4 +72,4 @@ export class EntityPermissions extends Component {
row.remove();
}
}
}

View File

@ -1,7 +1,8 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {Component} from './component';
export class EntitySearch extends Component {
setup() {
this.entityId = this.$opts.entityId;
this.entityType = this.$opts.entityType;
@ -30,7 +31,8 @@ export class EntitySearch extends Component {
runSearch() {
const term = this.searchInput.value.trim();
if (term.length === 0) {
return this.clearSearch();
this.clearSearch();
return;
}
this.searchView.classList.remove('hidden');
@ -51,4 +53,5 @@ export class EntitySearch extends Component {
this.loadingBlock.classList.add('hidden');
this.searchInput.value = '';
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class EntitySelectorPopup extends Component {
@ -57,4 +57,5 @@ export class EntitySelectorPopup extends Component {
this.getSelector().reset();
if (this.callback && entity) this.callback(entity);
}
}
}

View File

@ -1,5 +1,5 @@
import {onChildEvent} from "../services/dom";
import {Component} from "./component";
import {onChildEvent} from '../services/dom';
import {Component} from './component';
/**
* Entity Selector
@ -29,7 +29,7 @@ export class EntitySelector extends Component {
this.elem.addEventListener('click', this.onClick.bind(this));
let lastSearch = 0;
this.searchInput.addEventListener('input', event => {
this.searchInput.addEventListener('input', () => {
lastSearch = Date.now();
this.showLoading();
setTimeout(() => {
@ -43,35 +43,35 @@ export class EntitySelector extends Component {
});
// Keyboard navigation
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
if (e.ctrlKey && e.code === 'Enter') {
onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
if (event.ctrlKey && event.code === 'Enter') {
const form = this.$el.closest('form');
if (form) {
form.submit();
e.preventDefault();
event.preventDefault();
return;
}
}
if (e.code === 'ArrowDown') {
if (event.code === 'ArrowDown') {
this.focusAdjacent(true);
}
if (e.code === 'ArrowUp') {
if (event.code === 'ArrowUp') {
this.focusAdjacent(false);
}
});
this.searchInput.addEventListener('keydown', e => {
if (e.code === 'ArrowDown') {
this.searchInput.addEventListener('keydown', event => {
if (event.code === 'ArrowDown') {
this.focusAdjacent(true);
}
})
});
}
focusAdjacent(forward = true) {
const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
const selectedIndex = items.indexOf(document.activeElement);
const newItem = items[selectedIndex+ (forward ? 1 : -1)] || items[0];
const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
if (newItem) {
newItem.focus();
}
@ -101,7 +101,7 @@ export class EntitySelector extends Component {
window.$http.get(this.searchUrl()).then(resp => {
this.resultsContainer.innerHTML = resp.data;
this.hideLoading();
})
});
}
searchUrl() {
@ -144,13 +144,13 @@ export class EntitySelector extends Component {
const link = item.getAttribute('href');
const name = item.querySelector('.entity-list-item-name').textContent;
const data = {id: Number(id), name: name, link: link};
const data = {id: Number(id), name, link};
if (isSelected) {
item.classList.add('selected');
this.selectedItemData = data;
} else {
window.$events.emit('entity-select-change', null)
window.$events.emit('entity-select-change', null);
}
if (!isDblClick && !isSelected) return;
@ -159,7 +159,7 @@ export class EntitySelector extends Component {
this.confirmSelection(data);
}
if (isSelected) {
window.$events.emit('entity-select-change', data)
window.$events.emit('entity-select-change', data);
}
}
@ -175,4 +175,4 @@ export class EntitySelector extends Component {
this.selectedItemData = null;
}
}
}

View File

@ -1,5 +1,5 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {Component} from './component';
/**
* EventEmitSelect
@ -12,15 +12,15 @@ import {Component} from "./component";
* All options will be set as the "detail" of the event with
* their values included.
*/
export class EventEmitSelect extends Component{
export class EventEmitSelect extends Component {
setup() {
this.container = this.$el;
this.name = this.$opts.name;
onSelect(this.$el, () => {
this.$emit(this.name, this.$opts);
});
}
}
}

View File

@ -1,9 +1,9 @@
import {slideUp, slideDown} from "../services/animations";
import {Component} from "./component";
import {slideUp, slideDown} from '../services/animations';
import {Component} from './component';
export class ExpandToggle extends Component {
setup(elem) {
setup() {
this.targetSelector = this.$opts.targetSelector;
this.isOpen = this.$opts.isOpen === 'true';
this.updateEndpoint = this.$opts.updateEndpoint;
@ -24,8 +24,9 @@ export class ExpandToggle extends Component {
event.preventDefault();
const matchingElems = document.querySelectorAll(this.targetSelector);
for (let match of matchingElems) {
this.isOpen ? this.close(match) : this.open(match);
for (const match of matchingElems) {
const action = this.isOpen ? this.close : this.open;
action(match);
}
this.isOpen = !this.isOpen;
@ -34,8 +35,8 @@ export class ExpandToggle extends Component {
updateSystemAjax(isOpen) {
window.$http.patch(this.updateEndpoint, {
expand: isOpen ? 'true' : 'false'
expand: isOpen ? 'true' : 'false',
});
}
}
}

View File

@ -1,7 +1,7 @@
import {htmlToDom} from "../services/dom";
import {debounce} from "../services/util";
import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
import {Component} from "./component";
import {htmlToDom} from '../services/dom';
import {debounce} from '../services/util';
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
import {Component} from './component';
/**
* Global (header) search box handling.
@ -25,12 +25,12 @@ export class GlobalSearch extends Component {
// Handle search input changes
this.input.addEventListener('input', () => {
const value = this.input.value;
const {value} = this.input;
if (value.length > 0) {
this.loadingWrap.style.display = 'block';
this.suggestionResultsWrap.style.opacity = '0.5';
updateSuggestionsDebounced(value);
} else {
} else {
this.hideSuggestions();
}
});
@ -55,7 +55,7 @@ export class GlobalSearch extends Component {
if (!this.input.value) {
return;
}
const resultDom = htmlToDom(results);
this.suggestionResultsWrap.innerHTML = '';
@ -71,7 +71,7 @@ export class GlobalSearch extends Component {
this.container.classList.add('search-active');
window.requestAnimationFrame(() => {
this.suggestions.classList.add('search-suggestions-animation');
})
});
}
hideSuggestions() {
@ -79,4 +79,5 @@ export class GlobalSearch extends Component {
this.suggestions.classList.remove('search-suggestions-animation');
this.suggestionResultsWrap.innerHTML = '';
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class HeaderMobileToggle extends Component {
@ -19,10 +19,10 @@ export class HeaderMobileToggle extends Component {
this.toggleButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');
if (this.open) {
this.elem.addEventListener('keydown', this.onKeyDown);
window.addEventListener('click', this.onWindowClick)
window.addEventListener('click', this.onWindowClick);
} else {
this.elem.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('click', this.onWindowClick)
window.removeEventListener('click', this.onWindowClick);
}
event.stopPropagation();
}
@ -37,4 +37,4 @@ export class HeaderMobileToggle extends Component {
this.onToggle(event);
}
}
}

View File

@ -1,5 +1,7 @@
import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
import {Component} from "./component";
import {
onChildEvent, onSelect, removeLoading, showLoading,
} from '../services/dom';
import {Component} from './component';
export class ImageManager extends Component {
@ -48,20 +50,20 @@ export class ImageManager extends Component {
event.preventDefault();
});
onSelect(this.cancelSearch, event => {
onSelect(this.cancelSearch, () => {
this.resetListView();
this.resetSearchView();
this.loadGallery();
this.cancelSearch.classList.remove('active');
});
this.searchInput.addEventListener('input', event => {
this.searchInput.addEventListener('input', () => {
this.cancelSearch.classList.toggle('active', this.searchInput.value.trim());
});
onChildEvent(this.listContainer, '.load-more', 'click', async event => {
showLoading(event.target);
this.page++;
this.page += 1;
await this.loadGallery();
event.target.remove();
});
@ -69,7 +71,7 @@ export class ImageManager extends Component {
this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
this.listContainer.addEventListener('error', event => {
event.target.src = baseUrl('loading_error.png');
event.target.src = window.baseUrl('loading_error.png');
}, true);
onSelect(this.selectButton, () => {
@ -79,7 +81,7 @@ export class ImageManager extends Component {
this.hide();
});
onChildEvent(this.formContainer, '#image-manager-delete', 'click', event => {
onChildEvent(this.formContainer, '#image-manager-delete', 'click', () => {
if (this.lastSelected) {
this.loadImageEditForm(this.lastSelected.id, true);
}
@ -210,4 +212,4 @@ export class ImageManager extends Component {
window.$components.init(this.formContainer);
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class ImagePicker extends Component {
@ -31,7 +31,7 @@ export class ImagePicker extends Component {
this.removeInput.setAttribute('disabled', 'disabled');
}
for (let file of this.imageInput.files) {
for (const file of this.imageInput.files) {
this.imageElem.src = window.URL.createObjectURL(file);
}
this.imageElem.classList.remove('none');
@ -54,4 +54,4 @@ export class ImagePicker extends Component {
this.resetInput.setAttribute('disabled', 'disabled');
}
}
}

View File

@ -1,59 +1,59 @@
export {AddRemoveRows} from "./add-remove-rows.js"
export {AjaxDeleteRow} from "./ajax-delete-row.js"
export {AjaxForm} from "./ajax-form.js"
export {Attachments} from "./attachments.js"
export {AttachmentsList} from "./attachments-list.js"
export {AutoSuggest} from "./auto-suggest.js"
export {AutoSubmit} from "./auto-submit.js"
export {BackToTop} from "./back-to-top.js"
export {BookSort} from "./book-sort.js"
export {ChapterContents} from "./chapter-contents.js"
export {CodeEditor} from "./code-editor.js"
export {CodeHighlighter} from "./code-highlighter.js"
export {CodeTextarea} from "./code-textarea.js"
export {Collapsible} from "./collapsible.js"
export {ConfirmDialog} from "./confirm-dialog"
export {CustomCheckbox} from "./custom-checkbox.js"
export {DetailsHighlighter} from "./details-highlighter.js"
export {Dropdown} from "./dropdown.js"
export {DropdownSearch} from "./dropdown-search.js"
export {Dropzone} from "./dropzone.js"
export {EditorToolbox} from "./editor-toolbox.js"
export {EntityPermissions} from "./entity-permissions"
export {EntitySearch} from "./entity-search.js"
export {EntitySelector} from "./entity-selector.js"
export {EntitySelectorPopup} from "./entity-selector-popup.js"
export {EventEmitSelect} from "./event-emit-select.js"
export {ExpandToggle} from "./expand-toggle.js"
export {GlobalSearch} from "./global-search.js"
export {HeaderMobileToggle} from "./header-mobile-toggle.js"
export {ImageManager} from "./image-manager.js"
export {ImagePicker} from "./image-picker.js"
export {ListSortControl} from "./list-sort-control.js"
export {MarkdownEditor} from "./markdown-editor.js"
export {NewUserPassword} from "./new-user-password.js"
export {Notification} from "./notification.js"
export {OptionalInput} from "./optional-input.js"
export {PageComments} from "./page-comments.js"
export {PageDisplay} from "./page-display.js"
export {PageEditor} from "./page-editor.js"
export {PagePicker} from "./page-picker.js"
export {PermissionsTable} from "./permissions-table.js"
export {Pointer} from "./pointer.js"
export {Popup} from "./popup.js"
export {SettingAppColorScheme} from "./setting-app-color-scheme.js"
export {SettingColorPicker} from "./setting-color-picker.js"
export {SettingHomepageControl} from "./setting-homepage-control.js"
export {ShelfSort} from "./shelf-sort.js"
export {Shortcuts} from "./shortcuts"
export {ShortcutInput} from "./shortcut-input"
export {SortableList} from "./sortable-list.js"
export {SubmitOnChange} from "./submit-on-change.js"
export {Tabs} from "./tabs.js"
export {TagManager} from "./tag-manager.js"
export {TemplateManager} from "./template-manager.js"
export {ToggleSwitch} from "./toggle-switch.js"
export {TriLayout} from "./tri-layout.js"
export {UserSelect} from "./user-select.js"
export {WebhookEvents} from "./webhook-events"
export {WysiwygEditor} from "./wysiwyg-editor.js"
export {AddRemoveRows} from './add-remove-rows';
export {AjaxDeleteRow} from './ajax-delete-row';
export {AjaxForm} from './ajax-form';
export {Attachments} from './attachments';
export {AttachmentsList} from './attachments-list';
export {AutoSuggest} from './auto-suggest';
export {AutoSubmit} from './auto-submit';
export {BackToTop} from './back-to-top';
export {BookSort} from './book-sort';
export {ChapterContents} from './chapter-contents';
export {CodeEditor} from './code-editor';
export {CodeHighlighter} from './code-highlighter';
export {CodeTextarea} from './code-textarea';
export {Collapsible} from './collapsible';
export {ConfirmDialog} from './confirm-dialog';
export {CustomCheckbox} from './custom-checkbox';
export {DetailsHighlighter} from './details-highlighter';
export {Dropdown} from './dropdown';
export {DropdownSearch} from './dropdown-search';
export {Dropzone} from './dropzone';
export {EditorToolbox} from './editor-toolbox';
export {EntityPermissions} from './entity-permissions';
export {EntitySearch} from './entity-search';
export {EntitySelector} from './entity-selector';
export {EntitySelectorPopup} from './entity-selector-popup';
export {EventEmitSelect} from './event-emit-select';
export {ExpandToggle} from './expand-toggle';
export {GlobalSearch} from './global-search';
export {HeaderMobileToggle} from './header-mobile-toggle';
export {ImageManager} from './image-manager';
export {ImagePicker} from './image-picker';
export {ListSortControl} from './list-sort-control';
export {MarkdownEditor} from './markdown-editor';
export {NewUserPassword} from './new-user-password';
export {Notification} from './notification';
export {OptionalInput} from './optional-input';
export {PageComments} from './page-comments';
export {PageDisplay} from './page-display';
export {PageEditor} from './page-editor';
export {PagePicker} from './page-picker';
export {PermissionsTable} from './permissions-table';
export {Pointer} from './pointer';
export {Popup} from './popup';
export {SettingAppColorScheme} from './setting-app-color-scheme';
export {SettingColorPicker} from './setting-color-picker';
export {SettingHomepageControl} from './setting-homepage-control';
export {ShelfSort} from './shelf-sort';
export {Shortcuts} from './shortcuts';
export {ShortcutInput} from './shortcut-input';
export {SortableList} from './sortable-list';
export {SubmitOnChange} from './submit-on-change';
export {Tabs} from './tabs';
export {TagManager} from './tag-manager';
export {TemplateManager} from './template-manager';
export {ToggleSwitch} from './toggle-switch';
export {TriLayout} from './tri-layout';
export {UserSelect} from './user-select';
export {WebhookEvents} from './webhook-events';
export {WysiwygEditor} from './wysiwyg-editor';

View File

@ -2,7 +2,7 @@
* ListSortControl
* Manages the logic for the control which provides list sorting options.
*/
import {Component} from "./component";
import {Component} from './component';
export class ListSortControl extends Component {
@ -45,4 +45,4 @@ export class ListSortControl extends Component {
this.form.submit();
}
}
}

View File

@ -1,5 +1,5 @@
import {Component} from "./component";
import {init as initEditor} from "../markdown/editor";
import {Component} from './component';
import {init as initEditor} from '../markdown/editor';
export class MarkdownEditor extends Component {
@ -16,7 +16,7 @@ export class MarkdownEditor extends Component {
this.divider = this.$refs.divider;
this.displayWrap = this.$refs.displayWrap;
const settingContainer = this.$refs.settingContainer;
const {settingContainer} = this.$refs;
const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
this.editor = null;
@ -49,10 +49,9 @@ export class MarkdownEditor extends Component {
}
setupListeners() {
// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
const button = event.target.closest('button[data-action]');
if (button === null) return;
const action = button.getAttribute('data-action');
@ -83,15 +82,15 @@ export class MarkdownEditor extends Component {
}
handleDividerDrag() {
this.divider.addEventListener('pointerdown', event => {
this.divider.addEventListener('pointerdown', () => {
const wrapRect = this.elem.getBoundingClientRect();
const moveListener = (event) => {
const moveListener = event => {
const xRel = event.pageX - wrapRect.left;
const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
this.displayWrap.style.flexBasis = `${100-xPct}%`;
this.displayWrap.style.flexBasis = `${100 - xPct}%`;
this.editor.settings.set('editorWidth', xPct);
};
const upListener = (event) => {
const upListener = () => {
window.removeEventListener('pointermove', moveListener);
window.removeEventListener('pointerup', upListener);
this.display.style.pointerEvents = null;
@ -105,7 +104,7 @@ export class MarkdownEditor extends Component {
});
const widthSetting = this.editor.settings.get('editorWidth');
if (widthSetting) {
this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
this.displayWrap.style.flexBasis = `${100 - widthSetting}%`;
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class NewUserPassword extends Component {
@ -23,4 +23,4 @@ export class NewUserPassword extends Component {
this.inputContainer.style.display = inviting ? 'none' : 'block';
}
}
}

View File

@ -1,13 +1,13 @@
import {Component} from "./component";
import {Component} from './component';
export class Notification extends Component {
export class Notification extends Component {
setup() {
this.container = this.$el;
this.type = this.$opts.type;
this.textElem = this.container.querySelector('span');
this.autoHide = this.$opts.autoHide === 'true';
this.initialShow = this.$opts.show === 'true'
this.initialShow = this.$opts.show === 'true';
this.container.style.display = 'grid';
window.$events.listen(this.type, text => {
@ -47,4 +47,4 @@ export class Notification extends Component {
this.container.removeEventListener('transitionend', this.hideCleanup);
}
}
}

View File

@ -1,7 +1,8 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {onSelect} from '../services/dom';
import {Component} from './component';
export class OptionalInput extends Component {
setup() {
this.removeButton = this.$refs.remove;
this.showButton = this.$refs.show;
@ -24,4 +25,4 @@ export class OptionalInput extends Component {
});
}
}
}

View File

@ -1,6 +1,6 @@
import {scrollAndHighlightElement} from "../services/util";
import {Component} from "./component";
import {htmlToDom} from "../services/dom";
import {scrollAndHighlightElement} from '../services/util';
import {Component} from './component';
import {htmlToDom} from '../services/dom';
export class PageComments extends Component {
@ -36,11 +36,11 @@ export class PageComments extends Component {
}
handleAction(event) {
let actionElem = event.target.closest('[action]');
const actionElem = event.target.closest('[action]');
if (event.target.matches('a[href^="#"]')) {
const id = event.target.href.split('#')[1];
scrollAndHighlightElement(document.querySelector('#' + id));
scrollAndHighlightElement(document.querySelector(`#${id}`));
}
if (actionElem === null) return;
@ -68,24 +68,24 @@ export class PageComments extends Component {
if (this.editingComment) this.closeUpdateForm();
commentElem.querySelector('[comment-content]').style.display = 'none';
commentElem.querySelector('[comment-edit-container]').style.display = 'block';
let textArea = commentElem.querySelector('[comment-edit-container] textarea');
let lineCount = textArea.value.split('\n').length;
textArea.style.height = ((lineCount * 20) + 40) + 'px';
const textArea = commentElem.querySelector('[comment-edit-container] textarea');
const lineCount = textArea.value.split('\n').length;
textArea.style.height = `${(lineCount * 20) + 40}px`;
this.editingComment = commentElem;
}
updateComment(event) {
let form = event.target;
const form = event.target;
event.preventDefault();
let text = form.querySelector('textarea').value;
let reqData = {
text: text,
const text = form.querySelector('textarea').value;
const reqData = {
text,
parent_id: this.parentId || null,
};
this.showLoading(form);
let commentId = this.editingComment.getAttribute('comment');
const commentId = this.editingComment.getAttribute('comment');
window.$http.put(`/comment/${commentId}`, reqData).then(resp => {
let newComment = document.createElement('div');
const newComment = document.createElement('div');
newComment.innerHTML = resp.data;
this.editingComment.innerHTML = newComment.children[0].innerHTML;
window.$events.success(this.updatedText);
@ -98,9 +98,9 @@ export class PageComments extends Component {
}
deleteComment(commentElem) {
let id = commentElem.getAttribute('comment');
const id = commentElem.getAttribute('comment');
this.showLoading(commentElem.querySelector('[comment-content]'));
window.$http.delete(`/comment/${id}`).then(resp => {
window.$http.delete(`/comment/${id}`).then(() => {
commentElem.parentNode.removeChild(commentElem);
window.$events.success(this.deletedText);
this.updateCount();
@ -111,9 +111,9 @@ export class PageComments extends Component {
saveComment(event) {
event.preventDefault();
event.stopPropagation();
let text = this.formInput.value;
let reqData = {
text: text,
const text = this.formInput.value;
const reqData = {
text,
parent_id: this.parentId || null,
};
this.showLoading(this.form);
@ -131,7 +131,7 @@ export class PageComments extends Component {
}
updateCount() {
let count = this.container.children.length;
const count = this.container.children.length;
this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count});
}
@ -148,14 +148,14 @@ export class PageComments extends Component {
this.formContainer.parentNode.style.display = 'block';
this.addButtonContainer.style.display = 'none';
this.formInput.focus();
this.formInput.scrollIntoView({behavior: "smooth"});
this.formInput.scrollIntoView({behavior: 'smooth'});
}
hideForm() {
this.formContainer.style.display = 'none';
this.formContainer.parentNode.style.display = 'none';
if (this.getCommentCount() > 0) {
this.elem.appendChild(this.addButtonContainer)
this.elem.appendChild(this.addButtonContainer);
} else {
this.commentCountBar.appendChild(this.addButtonContainer);
}
@ -182,7 +182,7 @@ export class PageComments extends Component {
showLoading(formElem) {
const groups = formElem.querySelectorAll('.form-group');
for (let group of groups) {
for (const group of groups) {
group.style.display = 'none';
}
formElem.querySelector('.form-group.loading').style.display = 'block';
@ -190,10 +190,10 @@ export class PageComments extends Component {
hideLoading(formElem) {
const groups = formElem.querySelectorAll('.form-group');
for (let group of groups) {
for (const group of groups) {
group.style.display = 'block';
}
formElem.querySelector('.form-group.loading').style.display = 'none';
}
}
}

View File

@ -1,6 +1,33 @@
import * as DOM from "../services/dom";
import {scrollAndHighlightElement} from "../services/util";
import {Component} from "./component";
import * as DOM from '../services/dom';
import {scrollAndHighlightElement} from '../services/util';
import {Component} from './component';
function toggleAnchorHighlighting(elementId, shouldHighlight) {
DOM.forEach(`a[href="#${elementId}"]`, anchor => {
anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
});
}
function headingVisibilityChange(entries) {
for (const entry of entries) {
const isVisible = (entry.intersectionRatio === 1);
toggleAnchorHighlighting(entry.target.id, isVisible);
}
}
function addNavObserver(headings) {
// Setup the intersection observer.
const intersectOpts = {
rootMargin: '0px 0px 0px 0px',
threshold: 1.0,
};
const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
for (const heading of headings) {
pageNavObserver.observe(heading);
}
}
export class PageDisplay extends Component {
@ -26,7 +53,7 @@ export class PageDisplay extends Component {
window.$components.first('tri-layout').showContent();
const contentId = child.getAttribute('href').substr(1);
this.goToText(contentId);
window.history.pushState(null, null, '#' + contentId);
window.history.pushState(null, null, `#${contentId}`);
});
}
}
@ -58,33 +85,6 @@ export class PageDisplay extends Component {
if (headings.length > 0 && pageNav !== null) {
addNavObserver(headings);
}
function addNavObserver(headings) {
// Setup the intersection observer.
const intersectOpts = {
rootMargin: '0px 0px 0px 0px',
threshold: 1.0
};
const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
for (const heading of headings) {
pageNavObserver.observe(heading);
}
}
function headingVisibilityChange(entries, observer) {
for (const entry of entries) {
const isVisible = (entry.intersectionRatio === 1);
toggleAnchorHighlighting(entry.target.id, isVisible);
}
}
function toggleAnchorHighlighting(elementId, shouldHighlight) {
DOM.forEach('a[href="#' + elementId + '"]', anchor => {
anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
});
}
}
setupDetailsCodeBlockRefresh() {
@ -96,4 +96,5 @@ export class PageDisplay extends Component {
const details = [...this.container.querySelectorAll('details')];
details.forEach(detail => detail.addEventListener('toggle', onToggle));
}
}
}

View File

@ -1,9 +1,10 @@
import * as Dates from "../services/dates";
import {onSelect} from "../services/dom";
import {debounce} from "../services/util";
import {Component} from "./component";
import * as Dates from '../services/dates';
import {onSelect} from '../services/dom';
import {debounce} from '../services/util';
import {Component} from './component';
export class PageEditor extends Component {
setup() {
// Options
this.draftsEnabled = this.$opts.draftsEnabled === 'true';
@ -58,7 +59,9 @@ export class PageEditor extends Component {
window.$events.listen('editor-save-page', this.savePage.bind(this));
// Listen to content changes from the editor
const onContentChange = () => this.autoSave.pendingChange = true;
const onContentChange = () => {
this.autoSave.pendingChange = true;
};
window.$events.listen('editor-html-change', onContentChange);
window.$events.listen('editor-markdown-change', onContentChange);
@ -79,7 +82,8 @@ export class PageEditor extends Component {
setInitialFocus() {
if (this.hasDefaultTitle) {
return this.titleElem.select();
this.titleElem.select();
return;
}
window.setTimeout(() => {
@ -93,12 +97,12 @@ export class PageEditor extends Component {
runAutoSave() {
// Stop if manually saved recently to prevent bombarding the server
const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency) / 2);
if (savedRecently || !this.autoSave.pendingChange) {
return;
}
this.saveDraft()
this.saveDraft();
}
savePage() {
@ -132,7 +136,9 @@ export class PageEditor extends Component {
try {
const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
window.localStorage.setItem(saveKey, JSON.stringify(data));
} catch (err) {}
} catch (lsErr) {
console.error(lsErr);
}
window.$events.emit('error', this.autosaveFailText);
}
@ -153,7 +159,8 @@ export class PageEditor extends Component {
try {
response = await window.$http.get(`/ajax/page/${this.pageId}`);
} catch (e) {
return console.error(e);
console.error(e);
return;
}
if (this.autoSave.interval) {
@ -172,7 +179,6 @@ export class PageEditor extends Component {
this.startAutoSave();
}, 1000);
window.$events.emit('success', this.draftDiscardedText);
}
updateChangelogDisplay() {
@ -180,7 +186,7 @@ export class PageEditor extends Component {
if (summary.length === 0) {
summary = this.setChangelogText;
} else if (summary.length > 16) {
summary = summary.slice(0, 16) + '...';
summary = `${summary.slice(0, 16)}...`;
}
this.changelogDisplay.innerText = summary;
}
@ -193,7 +199,7 @@ export class PageEditor extends Component {
event.preventDefault();
const link = event.target.closest('a').href;
/** @var {ConfirmDialog} **/
/** @var {ConfirmDialog} * */
const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);

View File

@ -1,4 +1,8 @@
import {Component} from "./component";
import {Component} from './component';
function toggleElem(elem, show) {
elem.style.display = show ? null : 'none';
}
export class PagePicker extends Component {
@ -18,13 +22,13 @@ export class PagePicker extends Component {
this.selectButton.addEventListener('click', this.showPopup.bind(this));
this.display.parentElement.addEventListener('click', this.showPopup.bind(this));
this.resetButton.addEventListener('click', event => {
this.resetButton.addEventListener('click', () => {
this.setValue('', '');
});
}
showPopup() {
/** @type {EntitySelectorPopup} **/
/** @type {EntitySelectorPopup} * */
const selectorPopup = window.$components.first('entity-selector-popup');
selectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
@ -44,7 +48,7 @@ export class PagePicker extends Component {
toggleElem(this.defaultDisplay, !hasValue);
toggleElem(this.display, hasValue);
if (hasValue) {
let id = this.getAssetIdFromVal();
const id = this.getAssetIdFromVal();
this.display.textContent = `#${id}, ${name}`;
this.display.href = window.baseUrl(`/link/${id}`);
}
@ -55,7 +59,3 @@ export class PagePicker extends Component {
}
}
function toggleElem(elem, show) {
elem.style.display = show ? null : 'none';
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class PermissionsTable extends Component {
@ -41,7 +41,7 @@ export class PermissionsTable extends Component {
const tableRows = this.container.querySelectorAll(this.rowSelector);
const inputsToToggle = [];
for (let row of tableRows) {
for (const row of tableRows) {
const targetCell = row.children[colIndex];
if (targetCell) {
inputsToToggle.push(...targetCell.querySelectorAll('input[type=checkbox]'));
@ -57,10 +57,10 @@ export class PermissionsTable extends Component {
toggleAllInputs(inputsToToggle) {
const currentState = inputsToToggle.length > 0 ? inputsToToggle[0].checked : false;
for (let checkbox of inputsToToggle) {
for (const checkbox of inputsToToggle) {
checkbox.checked = !currentState;
checkbox.dispatchEvent(new Event('change'));
}
}
}
}

View File

@ -1,7 +1,6 @@
import * as DOM from "../services/dom";
import {Component} from "./component";
import {copyTextToClipboard} from "../services/clipboard";
import * as DOM from '../services/dom';
import {Component} from './component';
import {copyTextToClipboard} from '../services/clipboard';
export class Pointer extends Component {
@ -22,7 +21,7 @@ export class Pointer extends Component {
setupListeners() {
// Copy on copy button click
this.button.addEventListener('click', event => {
this.button.addEventListener('click', () => {
copyTextToClipboard(this.input.value);
});
@ -47,7 +46,7 @@ export class Pointer extends Component {
});
// Hide pointer when clicking away
DOM.onEvents(document.body, ['click', 'focus'], event => {
DOM.onEvents(document.body, ['click', 'focus'], () => {
if (!this.showing || this.isSelection) return;
this.hidePointer();
});
@ -113,7 +112,7 @@ export class Pointer extends Component {
updateForTarget(element) {
let inputText = this.pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${this.pointerSectionId}`) : `{{@${this.pageId}#${this.pointerSectionId}}}`;
if (this.pointerModeLink && !inputText.startsWith('http')) {
inputText = window.location.protocol + "//" + window.location.host + inputText;
inputText = `${window.location.protocol}//${window.location.host}${inputText}`;
}
this.input.value = inputText;
@ -121,7 +120,7 @@ export class Pointer extends Component {
// Update anchor if present
const editAnchor = this.container.querySelector('#pointer-edit');
if (editAnchor && element) {
const editHref = editAnchor.dataset.editHref;
const {editHref} = editAnchor.dataset;
const elementId = element.id;
// get the first 50 characters.
@ -129,4 +128,5 @@ export class Pointer extends Component {
editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
}
}
}
}

View File

@ -1,6 +1,6 @@
import {fadeIn, fadeOut} from "../services/animations";
import {onSelect} from "../services/dom";
import {Component} from "./component";
import {fadeIn, fadeOut} from '../services/animations';
import {onSelect} from '../services/dom';
import {Component} from './component';
/**
* Popup window that will contain other content.
@ -26,11 +26,11 @@ export class Popup extends Component {
this.container.addEventListener('click', event => {
if (event.target === this.container && lastMouseDownTarget === this.container) {
return this.hide();
this.hide();
}
});
onSelect(this.hideButtons, e => this.hide());
onSelect(this.hideButtons, () => this.hide());
}
hide(onComplete = null) {
@ -47,7 +47,7 @@ export class Popup extends Component {
show(onComplete = null, onHide = null) {
fadeIn(this.container, 120, onComplete);
this.onkeyup = (event) => {
this.onkeyup = event => {
if (event.key === 'Escape') {
this.hide();
}
@ -56,4 +56,4 @@ export class Popup extends Component {
this.onHide = onHide;
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class SettingAppColorScheme extends Component {
@ -14,7 +14,7 @@ export class SettingAppColorScheme extends Component {
this.handleModeChange(newMode);
});
const onInputChange = (event) => {
const onInputChange = event => {
this.updateAppColorsFromInputs();
if (event.target.name.startsWith('setting-app-color')) {
@ -44,7 +44,7 @@ export class SettingAppColorScheme extends Component {
cssId = 'primary';
}
const varName = '--color-' + cssId;
const varName = `--color-${cssId}`;
document.body.style.setProperty(varName, input.value);
}
}
@ -57,9 +57,8 @@ export class SettingAppColorScheme extends Component {
const lightName = input.name.replace('-color', '-color-light');
const hexVal = input.value;
const rgb = this.hexToRgb(hexVal);
const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
const rgbLightVal = `rgba(${[rgb.r, rgb.g, rgb.b, '0.15'].join(',')})`;
console.log(input.name, lightName, hexVal, rgbLightVal)
const lightColorInput = this.container.querySelector(`input[name="${lightName}"][type="hidden"]`);
lightColorInput.value = rgbLightVal;
}
@ -75,7 +74,7 @@ export class SettingAppColorScheme extends Component {
return {
r: result ? parseInt(result[1], 16) : 0,
g: result ? parseInt(result[2], 16) : 0,
b: result ? parseInt(result[3], 16) : 0
b: result ? parseInt(result[3], 16) : 0,
};
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class SettingColorPicker extends Component {
@ -17,4 +17,5 @@ export class SettingColorPicker extends Component {
this.colorInput.value = value;
this.colorInput.dispatchEvent(new Event('change', {bubbles: true}));
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class SettingHomepageControl extends Component {
@ -14,4 +14,5 @@ export class SettingHomepageControl extends Component {
const showPagePicker = this.typeControl.value === 'page';
this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
}
}
}

View File

@ -1,17 +1,17 @@
import Sortable from "sortablejs";
import {Component} from "./component";
import Sortable from 'sortablejs';
import {Component} from './component';
/**
* @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
*/
const itemActions = {
move_up(item, shelfBooksList, allBooksList) {
move_up(item) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.max(index - 1, 0);
list.insertBefore(item, list.children[newIndex] || null);
},
move_down(item, shelfBooksList, allBooksList) {
move_down(item) {
const list = item.parentNode;
const index = Array.from(list.children).indexOf(item);
const newIndex = Math.min(index + 2, list.children.length);
@ -20,7 +20,7 @@ const itemActions = {
remove(item, shelfBooksList, allBooksList) {
allBooksList.appendChild(item);
},
add(item, shelfBooksList, allBooksList) {
add(item, shelfBooksList) {
shelfBooksList.appendChild(item);
},
};
@ -62,11 +62,11 @@ export class ShelfSort extends Component {
}
});
this.bookSearchInput.addEventListener('input', event => {
this.bookSearchInput.addEventListener('input', () => {
this.filterBooksByName(this.bookSearchInput.value);
});
this.sortButtonContainer.addEventListener('click' , event => {
this.sortButtonContainer.addEventListener('click', event => {
const button = event.target.closest('button[data-sort]');
if (button) {
this.sortShelfBooks(button.dataset.sort);
@ -78,11 +78,10 @@ export class ShelfSort extends Component {
* @param {String} filterVal
*/
filterBooksByName(filterVal) {
// Set height on first search, if not already set, to prevent the distraction
// of the list height jumping around
if (!this.allBookList.style.height) {
this.allBookList.style.height = this.allBookList.getBoundingClientRect().height + 'px';
this.allBookList.style.height = `${this.allBookList.getBoundingClientRect().height}px`;
}
const books = this.allBookList.children;
@ -100,7 +99,7 @@ export class ShelfSort extends Component {
*/
sortItemActionClick(sortItemAction) {
const sortItem = sortItemAction.closest('.scroll-box-item');
const action = sortItemAction.dataset.action;
const {action} = sortItemAction.dataset;
const actionFunction = itemActions[action];
actionFunction(sortItem, this.shelfBookList, this.allBookList);
@ -122,10 +121,10 @@ export class ShelfSort extends Component {
const bProp = bookB.dataset[sortProperty].toLowerCase();
if (reverse) {
return aProp < bProp ? (aProp === bProp ? 0 : 1) : -1;
return bProp.localeCompare(aProp);
}
return aProp < bProp ? (aProp === bProp ? 0 : -1) : 1;
return aProp.localeCompare(bProp);
});
for (const book of books) {
@ -136,4 +135,4 @@ export class ShelfSort extends Component {
this.onChange();
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
/**
* Keys to ignore when recording shortcuts.
@ -18,16 +18,16 @@ export class ShortcutInput extends Component {
this.listenerRecordKey = this.listenerRecordKey.bind(this);
this.input.addEventListener('focus', () => {
this.startListeningForInput();
this.startListeningForInput();
});
this.input.addEventListener('blur', () => {
this.stopListeningForInput();
})
});
}
startListeningForInput() {
this.input.addEventListener('keydown', this.listenerRecordKey)
this.input.addEventListener('keydown', this.listenerRecordKey);
}
/**
@ -51,4 +51,4 @@ export class ShortcutInput extends Component {
this.input.removeEventListener('keydown', this.listenerRecordKey);
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
function reverseMap(map) {
const reversed = {};
@ -8,7 +8,6 @@ function reverseMap(map) {
return reversed;
}
export class Shortcuts extends Component {
setup() {
@ -25,7 +24,6 @@ export class Shortcuts extends Component {
setupListeners() {
window.addEventListener('keydown', event => {
if (event.target.closest('input, select, textarea')) {
return;
}
@ -35,7 +33,8 @@ export class Shortcuts extends Component {
window.addEventListener('keydown', event => {
if (event.key === '?') {
this.hintsShowing ? this.hideHints() : this.showHints();
const action = this.hintsShowing ? this.hideHints : this.showHints;
action();
}
});
}
@ -44,7 +43,6 @@ export class Shortcuts extends Component {
* @param {KeyboardEvent} event
*/
handleShortcutPress(event) {
const keys = [
event.ctrlKey ? 'Ctrl' : '',
event.metaKey ? 'Cmd' : '',
@ -90,7 +88,7 @@ export class Shortcuts extends Component {
return true;
}
console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
console.error('Shortcut attempted to be ran for element type that does not have handling setup', el);
return false;
}
@ -135,10 +133,10 @@ export class Shortcuts extends Component {
const linkage = document.createElement('div');
linkage.classList.add('shortcut-linkage');
linkage.style.left = targetBounds.x + 'px';
linkage.style.top = targetBounds.y + 'px';
linkage.style.width = targetBounds.width + 'px';
linkage.style.height = targetBounds.height + 'px';
linkage.style.left = `${targetBounds.x}px`;
linkage.style.top = `${targetBounds.y}px`;
linkage.style.width = `${targetBounds.width}px`;
linkage.style.height = `${targetBounds.height}px`;
wrapper.append(label, linkage);
@ -159,4 +157,5 @@ export class Shortcuts extends Component {
this.hintsShowing = false;
}
}
}

View File

@ -1,5 +1,5 @@
import Sortable from "sortablejs";
import {Component} from "./component";
import Sortable from 'sortablejs';
import {Component} from './component';
/**
* SortableList
@ -9,6 +9,7 @@ import {Component} from "./component";
* the data to set on the data-transfer.
*/
export class SortableList extends Component {
setup() {
this.container = this.$el;
this.handleSelector = this.$opts.handleSelector;
@ -33,4 +34,5 @@ export class SortableList extends Component {
dragoverBubble: false,
});
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
/**
* Submit on change
@ -9,8 +9,7 @@ export class SubmitOnChange extends Component {
setup() {
this.filter = this.$opts.filter;
this.$el.addEventListener('change', (event) => {
this.$el.addEventListener('change', event => {
if (this.filter && !event.target.matches(this.filter)) {
return;
}
@ -22,4 +21,4 @@ export class SubmitOnChange extends Component {
});
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
/**
* Tabs
@ -46,4 +46,4 @@ export class Tabs extends Component {
this.$emit('change', {showing: sectionId});
}
}
}

View File

@ -1,6 +1,7 @@
import {Component} from "./component";
import {Component} from './component';
export class TagManager extends Component {
setup() {
this.addRemoveComponentEl = this.$refs.addRemove;
this.container = this.$el;
@ -11,8 +12,7 @@ export class TagManager extends Component {
setupListeners() {
this.container.addEventListener('input', event => {
/** @var {AddRemoveRows} **/
/** @var {AddRemoveRows} * */
const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
if (!this.hasEmptyRows() && event.target.value) {
addRemoveComponent.add();
@ -22,9 +22,8 @@ export class TagManager extends Component {
hasEmptyRows() {
const rows = this.container.querySelectorAll(this.rowSelector);
const firstEmpty = [...rows].find(row => {
return [...row.querySelectorAll('input')].filter(input => input.value).length === 0;
});
const firstEmpty = [...rows].find(row => [...row.querySelectorAll('input')].filter(input => input.value).length === 0);
return firstEmpty !== undefined;
}
}
}

View File

@ -1,5 +1,5 @@
import * as DOM from "../services/dom";
import {Component} from "./component";
import * as DOM from '../services/dom';
import {Component} from './component';
export class TemplateManager extends Component {
@ -36,10 +36,10 @@ export class TemplateManager extends Component {
});
// Search submit button press
this.searchButton.addEventListener('click', event => this.performSearch());
this.searchButton.addEventListener('click', () => this.performSearch());
// Search cancel button press
this.searchCancel.addEventListener('click', event => {
this.searchCancel.addEventListener('click', () => {
this.searchInput.value = '';
this.performSearch();
});
@ -66,7 +66,7 @@ export class TemplateManager extends Component {
async insertTemplate(templateId, action = 'replace') {
const resp = await window.$http.get(`/templates/${templateId}`);
const eventName = 'editor::' + action;
const eventName = `editor::${action}`;
window.$events.emit(eventName, resp.data);
}
@ -79,10 +79,11 @@ export class TemplateManager extends Component {
async performSearch() {
const searchTerm = this.searchInput.value;
const resp = await window.$http.get(`/templates`, {
search: searchTerm
const resp = await window.$http.get('/templates', {
search: searchTerm,
});
this.searchCancel.style.display = searchTerm ? 'block' : 'none';
this.list.innerHTML = resp.data;
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class ToggleSwitch extends Component {
@ -18,4 +18,4 @@ export class ToggleSwitch extends Component {
this.input.dispatchEvent(changeEvent);
}
}
}

View File

@ -1,4 +1,4 @@
import {Component} from "./component";
import {Component} from './component';
export class TriLayout extends Component {
@ -9,8 +9,8 @@ export class TriLayout extends Component {
this.lastLayoutType = 'none';
this.onDestroy = null;
this.scrollCache = {
'content': 0,
'info': 0,
content: 0,
info: 0,
};
this.lastTabShown = 'content';
@ -19,15 +19,15 @@ export class TriLayout extends Component {
// Watch layout changes
this.updateLayout();
window.addEventListener('resize', event => {
window.addEventListener('resize', () => {
this.updateLayout();
}, {passive: true});
}
updateLayout() {
let newLayout = 'tablet';
if (window.innerWidth <= 1000) newLayout = 'mobile';
if (window.innerWidth >= 1400) newLayout = 'desktop';
if (window.innerWidth <= 1000) newLayout = 'mobile';
if (window.innerWidth >= 1400) newLayout = 'desktop';
if (newLayout === this.lastLayoutType) return;
if (this.onDestroy) {
@ -53,20 +53,19 @@ export class TriLayout extends Component {
for (const tab of this.tabs) {
tab.removeEventListener('click', this.mobileTabClick);
}
}
};
}
setupDesktop() {
//
}
/**
* Action to run when the mobile info toggle bar is clicked/tapped
* @param event
*/
mobileTabClick(event) {
const tab = event.target.dataset.tab;
const {tab} = event.target.dataset;
this.showTab(tab);
}
@ -109,4 +108,4 @@ export class TriLayout extends Component {
this.lastTabShown = tabName;
}
}
}

View File

@ -1,5 +1,5 @@
import {onChildEvent} from "../services/dom";
import {Component} from "./component";
import {onChildEvent} from '../services/dom';
import {Component} from './component';
export class UserSelect extends Component {
@ -20,9 +20,9 @@ export class UserSelect extends Component {
}
hide() {
/** @var {Dropdown} **/
/** @var {Dropdown} * */
const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
dropdown.hide();
}
}
}

View File

@ -2,7 +2,7 @@
* Webhook Events
* Manages dynamic selection control in the webhook form interface.
*/
import {Component} from "./component";
import {Component} from './component';
export class WebhookEvents extends Component {
@ -27,4 +27,4 @@ export class WebhookEvents extends Component {
}
}
}
}

View File

@ -1,5 +1,5 @@
import {build as buildEditorConfig} from "../wysiwyg/config";
import {Component} from "./component";
import {build as buildEditorConfig} from '../wysiwyg/config';
import {Component} from './component';
export class WysiwygEditor extends Component {
@ -45,8 +45,8 @@ export class WysiwygEditor extends Component {
*/
getContent() {
return {
html: this.editor.getContent()
html: this.editor.getContent(),
};
}
}
}

View File

@ -1,6 +1,7 @@
import DrawIO from "../services/drawio";
import * as DrawIO from '../services/drawio';
export class Actions {
/**
* @param {MarkdownEditor} editor
*/
@ -29,13 +30,13 @@ export class Actions {
}
showImageInsert() {
/** @type {ImageManager} **/
/** @type {ImageManager} * */
const imageManager = window.$components.first('image-manager');
imageManager.show(image => {
const imageUrl = image.thumbs.display || image.url;
const selectedText = this.#getSelectionText();
const newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`;
this.#replaceSelection(newText, newText.length);
}, 'gallery');
}
@ -49,12 +50,12 @@ export class Actions {
const selectedText = this.#getSelectionText();
const newText = `[${selectedText}]()`;
const cursorPosDiff = (selectedText === '') ? -3 : -1;
this.#replaceSelection(newText, newText.length+cursorPosDiff);
this.#replaceSelection(newText, newText.length + cursorPosDiff);
}
showImageManager() {
const selectionRange = this.#getSelectionRange();
/** @type {ImageManager} **/
/** @type {ImageManager} * */
const imageManager = window.$components.first('image-manager');
imageManager.show(image => {
this.#insertDrawing(image, selectionRange);
@ -65,7 +66,7 @@ export class Actions {
showLinkSelector() {
const selectionRange = this.#getSelectionRange();
/** @type {EntitySelectorPopup} **/
/** @type {EntitySelectorPopup} * */
const selector = window.$components.first('entity-selector-popup');
selector.show(entity => {
const selectedText = this.#getSelectionText(selectionRange) || entity.name;
@ -81,16 +82,13 @@ export class Actions {
const selectionRange = this.#getSelectionRange();
DrawIO.show(url,() => {
return Promise.resolve('');
}, (pngData) => {
DrawIO.show(url, () => Promise.resolve(''), pngData => {
const data = {
image: pngData,
uploaded_to: Number(this.editor.config.pageId),
};
window.$http.post("/images/drawio", data).then(resp => {
window.$http.post('/images/drawio', data).then(resp => {
this.#insertDrawing(resp.data, selectionRange);
DrawIO.close();
}).catch(err => {
@ -106,7 +104,7 @@ export class Actions {
// Show draw.io if enabled and handle save.
editDrawing(imgContainer) {
const drawioUrl = this.editor.config.drawioUrl;
const {drawioUrl} = this.editor.config;
if (!drawioUrl) {
return;
}
@ -114,16 +112,13 @@ export class Actions {
const selectionRange = this.#getSelectionRange();
const drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(drawioUrl, () => {
return DrawIO.load(drawingId);
}, (pngData) => {
DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), pngData => {
const data = {
image: pngData,
uploaded_to: Number(this.editor.config.pageId),
};
window.$http.post("/images/drawio", data).then(resp => {
window.$http.post('/images/drawio', data).then(resp => {
const newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
const newContent = this.#getText().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
@ -145,12 +140,12 @@ export class Actions {
} else {
window.$events.emit('error', this.editor.config.text.imageUploadError);
}
console.log(error);
console.error(error);
}
// Make the editor full screen
fullScreen() {
const container = this.editor.config.container;
const {container} = this.editor.config;
const alreadyFullscreen = container.classList.contains('fullscreen');
container.classList.toggle('fullscreen', !alreadyFullscreen);
document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
@ -170,7 +165,7 @@ export class Actions {
scrollToLine = lineCount;
break;
}
lineCount++;
lineCount += 1;
}
if (scrollToLine === -1) {
@ -204,7 +199,7 @@ export class Actions {
content = this.#cleanTextForEditor(content);
const selectionRange = this.#getSelectionRange();
const selectFrom = selectionRange.from + content.length + 1;
this.#dispatchChange(0, 0, content + '\n', selectFrom);
this.#dispatchChange(0, 0, `${content}\n`, selectFrom);
this.focus();
}
@ -214,7 +209,7 @@ export class Actions {
*/
appendContent(content) {
content = this.#cleanTextForEditor(content);
this.#dispatchChange(this.editor.cm.state.doc.length, '\n' + content);
this.#dispatchChange(this.editor.cm.state.doc.length, `\n${content}`);
this.focus();
}
@ -223,7 +218,7 @@ export class Actions {
* @param {String} content
*/
replaceContent(content) {
this.#setText(content)
this.#setText(content);
}
/**
@ -250,7 +245,7 @@ export class Actions {
if (alreadySymbol) {
newLineContent = lineContent.replace(lineStart, newStart).trim();
} else if (newStart !== '') {
newLineContent = newStart + ' ' + lineContent;
newLineContent = `${newStart} ${lineContent}`;
}
const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
@ -263,22 +258,31 @@ export class Actions {
* @param {String} end
*/
wrapSelection(start, end) {
const selectionRange = this.#getSelectionRange();
const selectionText = this.#getSelectionText(selectionRange);
if (!selectionText) return this.#wrapLine(start, end);
const selectRange = this.#getSelectionRange();
const selectionText = this.#getSelectionText(selectRange);
if (!selectionText) {
this.#wrapLine(start, end);
return;
}
let newSelectionText = selectionText;
let newRange;
if (selectionText.startsWith(start) && selectionText.endsWith(end)) {
newSelectionText = selectionText.slice(start.length, selectionText.length - end.length);
newRange = selectionRange.extend(selectionRange.from, selectionRange.to - (start.length + end.length));
newRange = selectRange.extend(selectRange.from, selectRange.to - (start.length + end.length));
} else {
newSelectionText = `${start}${selectionText}${end}`;
newRange = selectionRange.extend(selectionRange.from, selectionRange.to + (start.length + end.length));
newRange = selectRange.extend(selectRange.from, selectRange.to + (start.length + end.length));
}
this.#dispatchChange(selectionRange.from, selectionRange.to, newSelectionText, newRange.anchor, newRange.head);
this.#dispatchChange(
selectRange.from,
selectRange.to,
newSelectionText,
newRange.anchor,
newRange.head,
);
}
replaceLineStartForOrderedList() {
@ -290,7 +294,7 @@ export class Actions {
const number = (Number(listMatch[2]) || 0) + 1;
const whiteSpace = listMatch[1] || '';
const listMark = listMatch[3] || '.'
const listMark = listMatch[3] || '.';
const prefix = `${whiteSpace}${number}${listMark}`;
return this.replaceLineStart(prefix);
@ -319,7 +323,13 @@ export class Actions {
const newFormat = formats[newFormatIndex];
const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat));
const lineDiff = newContent.length - line.text.length;
this.#dispatchChange(line.from, line.to, newContent, selectionRange.anchor + lineDiff, selectionRange.head + lineDiff);
this.#dispatchChange(
line.from,
line.to,
newContent,
selectionRange.anchor + lineDiff,
selectionRange.head + lineDiff,
);
}
}
@ -373,7 +383,7 @@ export class Actions {
* @param {File} file
* @param {?Number} position
*/
async uploadImage(file, position= null) {
async uploadImage(file, position = null) {
if (file === null || file.type.indexOf('image') !== 0) return;
let ext = 'png';
@ -382,17 +392,17 @@ export class Actions {
}
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
const fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
// Insert image into markdown
const id = "image-" + Math.random().toString(16).slice(2);
const id = `image-${Math.random().toString(16).slice(2)}`;
const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
const placeHolderText = `![](${placeholderImage})`;
this.#dispatchChange(position, position, placeHolderText, position);
const remoteFilename = "image-" + Date.now() + "." + ext;
const remoteFilename = `image-${Date.now()}.${ext}`;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', this.editor.config.pageId);
@ -404,7 +414,7 @@ export class Actions {
} catch (err) {
window.$events.emit('error', this.editor.config.text.imageUploadError);
this.#findAndReplaceContent(placeHolderText, '');
console.log(err);
console.error(err);
}
}
@ -437,7 +447,8 @@ export class Actions {
*/
#replaceSelection(newContent, cursorOffset = 0, selectionRange = null) {
selectionRange = selectionRange || this.editor.cm.state.selection.main;
this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectionRange.from + cursorOffset);
const selectFrom = selectionRange.from + cursorOffset;
this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
this.focus();
}
@ -466,7 +477,7 @@ export class Actions {
* @return {String}
*/
#cleanTextForEditor(text) {
return text.replace(/\r\n|\r/g, "\n");
return text.replace(/\r\n|\r/g, '\n');
}
/**
@ -511,10 +522,13 @@ export class Actions {
* @param {?Number} selectTo
*/
#dispatchChange(from, to = null, text = null, selectFrom = null, selectTo = null) {
const tr = {changes: {from, to: to, insert: text}};
const tr = {changes: {from, to, insert: text}};
if (selectFrom) {
tr.selection = {anchor: selectFrom};
if (selectTo) {
tr.selection.head = selectTo;
}
}
this.editor.cm.dispatch(tr);
@ -533,4 +547,5 @@ export class Actions {
scrollIntoView,
});
}
}
}

View File

@ -1,6 +1,6 @@
import {provideKeyBindings} from "./shortcuts";
import {debounce} from "../services/util";
import Clipboard from "../services/clipboard";
import {provideKeyBindings} from './shortcuts';
import {debounce} from '../services/util';
import {Clipboard} from '../services/clipboard';
/**
* Initiate the codemirror instance for the markdown editor.
@ -21,13 +21,15 @@ export async function init(editor) {
const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
let syncActive = editor.settings.get('scrollSync');
editor.settings.onChange('scrollSync', val => syncActive = val);
editor.settings.onChange('scrollSync', val => {
syncActive = val;
});
const domEventHandlers = {
// Handle scroll to sync display view
scroll: (event) => syncActive && onScrollDebounced(event),
scroll: event => syncActive && onScrollDebounced(event),
// Handle image & content drag n drop
drop: (event) => {
drop: event => {
const templateId = event.dataTransfer.getData('bookstack/template');
if (templateId) {
event.preventDefault();
@ -43,7 +45,7 @@ export async function init(editor) {
}
},
// Handle image paste
paste: (event) => {
paste: event => {
const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
// Don't handle the event ourselves if no items exist of contains table-looking data
@ -55,8 +57,8 @@ export async function init(editor) {
for (const image of images) {
editor.actions.uploadImage(image);
}
}
}
},
};
const cm = Code.markdownEditor(
editor.config.inputEl,
@ -70,4 +72,4 @@ export async function init(editor) {
window.mdEditorView = cm;
return cm;
}
}

View File

@ -6,23 +6,22 @@ function getContentToInsert({html, markdown}) {
* @param {MarkdownEditor} editor
*/
export function listen(editor) {
window.$events.listen('editor::replace', (eventContent) => {
window.$events.listen('editor::replace', eventContent => {
const markdown = getContentToInsert(eventContent);
editor.actions.replaceContent(markdown);
});
window.$events.listen('editor::append', (eventContent) => {
window.$events.listen('editor::append', eventContent => {
const markdown = getContentToInsert(eventContent);
editor.actions.appendContent(markdown);
});
window.$events.listen('editor::prepend', (eventContent) => {
window.$events.listen('editor::prepend', eventContent => {
const markdown = getContentToInsert(eventContent);
editor.actions.prependContent(markdown);
});
window.$events.listen('editor::insert', (eventContent) => {
window.$events.listen('editor::insert', eventContent => {
const markdown = getContentToInsert(eventContent);
editor.actions.insertContent(markdown);
});
@ -30,4 +29,4 @@ export function listen(editor) {
window.$events.listen('editor::focus', () => {
editor.actions.focus();
});
}
}

View File

@ -1,4 +1,4 @@
import {patchDomFromHtmlString} from "../services/vdom";
import {patchDomFromHtmlString} from '../services/vdom';
export class Display {
@ -81,7 +81,7 @@ export class Display {
* @param {String} html
*/
patchWithHtml(html) {
const body = this.doc.body;
const {body} = this.doc;
if (body.children.length === 0) {
const wrap = document.createElement('div');
@ -102,8 +102,8 @@ export class Display {
const elems = this.doc.body?.children[0]?.children;
if (elems && elems.length <= index) return;
const topElem = (index === -1) ? elems[elems.length-1] : elems[index];
topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];
topElem.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'smooth'});
}
}
}

View File

@ -1,10 +1,9 @@
import {Markdown} from "./markdown";
import {Display} from "./display";
import {Actions} from "./actions";
import {Settings} from "./settings";
import {listen} from "./common-events";
import {init as initCodemirror} from "./codemirror";
import {Markdown} from './markdown';
import {Display} from './display';
import {Actions} from './actions';
import {Settings} from './settings';
import {listen} from './common-events';
import {init as initCodemirror} from './codemirror';
/**
* Initiate a new markdown editor instance.
@ -12,7 +11,6 @@ import {init as initCodemirror} from "./codemirror";
* @returns {Promise<MarkdownEditor>}
*/
export async function init(config) {
/**
* @type {MarkdownEditor}
*/
@ -31,7 +29,6 @@ export async function init(config) {
return editor;
}
/**
* @typedef MarkdownEditorConfig
* @property {String} pageId
@ -51,4 +48,4 @@ export async function init(config) {
* @property {Actions} actions
* @property {EditorView} cm
* @property {Settings} settings
*/
*/

View File

@ -1,4 +1,4 @@
import MarkdownIt from "markdown-it";
import MarkdownIt from 'markdown-it';
import mdTasksLists from 'markdown-it-task-lists';
export class Markdown {
@ -24,7 +24,5 @@ export class Markdown {
render(markdown) {
return this.renderer.render(markdown);
}
}

View File

@ -21,7 +21,7 @@ export class Settings {
listenToInputChanges(inputs) {
for (const input of inputs) {
input.addEventListener('change', event => {
input.addEventListener('change', () => {
const name = input.getAttribute('name').replace('md-', '');
this.set(name, input.checked);
});
@ -59,4 +59,5 @@ export class Settings {
listeners.push(callback);
this.changeListeners[key] = listeners;
}
}
}

View File

@ -7,35 +7,35 @@ function provide(editor) {
const shortcuts = {};
// Insert Image shortcut
shortcuts['Shift-Mod-i'] = cm => editor.actions.insertImage();
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
// Save draft
shortcuts['Mod-s'] = cm => window.$events.emit('editor-save-draft');
shortcuts['Mod-s'] = () => window.$events.emit('editor-save-draft');
// Save page
shortcuts['Mod-Enter'] = cm => window.$events.emit('editor-save-page');
shortcuts['Mod-Enter'] = () => window.$events.emit('editor-save-page');
// Show link selector
shortcuts['Shift-Mod-k'] = cm => editor.actions.showLinkSelector();
shortcuts['Shift-Mod-k'] = () => editor.actions.showLinkSelector();
// Insert Link
shortcuts['Mod-k'] = cm => editor.actions.insertLink();
shortcuts['Mod-k'] = () => editor.actions.insertLink();
// FormatShortcuts
shortcuts['Mod-1'] = cm => editor.actions.replaceLineStart('##');
shortcuts['Mod-2'] = cm => editor.actions.replaceLineStart('###');
shortcuts['Mod-3'] = cm => editor.actions.replaceLineStart('####');
shortcuts['Mod-4'] = cm => editor.actions.replaceLineStart('#####');
shortcuts['Mod-5'] = cm => editor.actions.replaceLineStart('');
shortcuts['Mod-d'] = cm => editor.actions.replaceLineStart('');
shortcuts['Mod-6'] = cm => editor.actions.replaceLineStart('>');
shortcuts['Mod-q'] = cm => editor.actions.replaceLineStart('>');
shortcuts['Mod-7'] = cm => editor.actions.wrapSelection('\n```\n', '\n```');
shortcuts['Mod-8'] = cm => editor.actions.wrapSelection('`', '`');
shortcuts['Shift-Mod-e'] = cm => editor.actions.wrapSelection('`', '`');
shortcuts['Mod-9'] = cm => editor.actions.cycleCalloutTypeAtSelection();
shortcuts['Mod-p'] = cm => editor.actions.replaceLineStart('-')
shortcuts['Mod-o'] = cm => editor.actions.replaceLineStartForOrderedList()
shortcuts['Mod-1'] = () => editor.actions.replaceLineStart('##');
shortcuts['Mod-2'] = () => editor.actions.replaceLineStart('###');
shortcuts['Mod-3'] = () => editor.actions.replaceLineStart('####');
shortcuts['Mod-4'] = () => editor.actions.replaceLineStart('#####');
shortcuts['Mod-5'] = () => editor.actions.replaceLineStart('');
shortcuts['Mod-d'] = () => editor.actions.replaceLineStart('');
shortcuts['Mod-6'] = () => editor.actions.replaceLineStart('>');
shortcuts['Mod-q'] = () => editor.actions.replaceLineStart('>');
shortcuts['Mod-7'] = () => editor.actions.wrapSelection('\n```\n', '\n```');
shortcuts['Mod-8'] = () => editor.actions.wrapSelection('`', '`');
shortcuts['Shift-Mod-e'] = () => editor.actions.wrapSelection('`', '`');
shortcuts['Mod-9'] = () => editor.actions.cycleCalloutTypeAtSelection();
shortcuts['Mod-p'] = () => editor.actions.replaceLineStart('-');
shortcuts['Mod-o'] = () => editor.actions.replaceLineStartForOrderedList();
return shortcuts;
}
@ -46,14 +46,12 @@ function provide(editor) {
* @return {{key: String, run: function, preventDefault: boolean}[]}
*/
export function provideKeyBindings(editor) {
const shortcuts= provide(editor);
const shortcuts = provide(editor);
const keyBindings = [];
const wrapAction = (action) => {
return () => {
action();
return true;
};
const wrapAction = action => () => {
action();
return true;
};
for (const [shortcut, action] of Object.entries(shortcuts)) {
@ -61,4 +59,4 @@ export function provideKeyBindings(editor) {
}
return keyBindings;
}
}

View File

@ -5,6 +5,53 @@
*/
const animateStylesCleanupMap = new WeakMap();
/**
* Animate the css styles of an element using FLIP animation techniques.
* Styles must be an object where the keys are style properties, camelcase, and the values
* are an array of two items in the format [initialValue, finalValue]
* @param {Element} element
* @param {Object} styles
* @param {Number} animTime
* @param {Function} onComplete
*/
function animateStyles(element, styles, animTime = 400, onComplete = null) {
const styleNames = Object.keys(styles);
for (const style of styleNames) {
element.style[style] = styles[style][0];
}
const cleanup = () => {
for (const style of styleNames) {
element.style[style] = null;
}
element.style.transition = null;
element.removeEventListener('transitionend', cleanup);
animateStylesCleanupMap.delete(element);
if (onComplete) onComplete();
};
setTimeout(() => {
element.style.transition = `all ease-in-out ${animTime}ms`;
for (const style of styleNames) {
element.style[style] = styles[style][1];
}
element.addEventListener('transitionend', cleanup);
animateStylesCleanupMap.set(element, cleanup);
}, 15);
}
/**
* Run the active cleanup action for the given element.
* @param {Element} element
*/
function cleanupExistingElementAnimation(element) {
if (animateStylesCleanupMap.has(element)) {
const oldCleanup = animateStylesCleanupMap.get(element);
oldCleanup();
}
}
/**
* Fade in the given element.
* @param {Element} element
@ -15,7 +62,7 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
cleanupExistingElementAnimation(element);
element.style.display = 'block';
animateStyles(element, {
opacity: ['0', '1']
opacity: ['0', '1'],
}, animTime, () => {
if (onComplete) onComplete();
});
@ -30,7 +77,7 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
export function fadeOut(element, animTime = 400, onComplete = null) {
cleanupExistingElementAnimation(element);
animateStyles(element, {
opacity: ['1', '0']
opacity: ['1', '0'],
}, animTime, () => {
element.style.display = 'none';
if (onComplete) onComplete();
@ -113,50 +160,3 @@ export function transitionHeight(element, animTime = 400) {
animateStyles(element, animStyles, animTime);
};
}
/**
* Animate the css styles of an element using FLIP animation techniques.
* Styles must be an object where the keys are style properties, camelcase, and the values
* are an array of two items in the format [initialValue, finalValue]
* @param {Element} element
* @param {Object} styles
* @param {Number} animTime
* @param {Function} onComplete
*/
function animateStyles(element, styles, animTime = 400, onComplete = null) {
const styleNames = Object.keys(styles);
for (let style of styleNames) {
element.style[style] = styles[style][0];
}
const cleanup = () => {
for (let style of styleNames) {
element.style[style] = null;
}
element.style.transition = null;
element.removeEventListener('transitionend', cleanup);
animateStylesCleanupMap.delete(element);
if (onComplete) onComplete();
};
setTimeout(() => {
element.style.transition = `all ease-in-out ${animTime}ms`;
for (let style of styleNames) {
element.style[style] = styles[style][1];
}
element.addEventListener('transitionend', cleanup);
animateStylesCleanupMap.set(element, cleanup);
}, 15);
}
/**
* Run the active cleanup action for the given element.
* @param {Element} element
*/
function cleanupExistingElementAnimation(element) {
if (animateStylesCleanupMap.has(element)) {
const oldCleanup = animateStylesCleanupMap.get(element);
oldCleanup();
}
}

View File

@ -1,4 +1,3 @@
export class Clipboard {
/**
@ -21,7 +20,7 @@ export class Clipboard {
* @return {boolean}
*/
containsTabularData() {
const rtfData = this.data.getData( 'text/rtf');
const rtfData = this.data.getData('text/rtf');
return rtfData && rtfData.includes('\\trowd');
}
@ -30,8 +29,8 @@ export class Clipboard {
* @return {Array<File>}
*/
getImages() {
const types = this.data.types;
const files = this.data.files;
const {types} = this.data;
const {files} = this.data;
const images = [];
for (const type of types) {
@ -49,6 +48,7 @@ export class Clipboard {
return images;
}
}
export async function copyTextToClipboard(text) {
@ -58,13 +58,11 @@ export async function copyTextToClipboard(text) {
}
// Backup option where we can't use the navigator.clipboard API
const tempInput = document.createElement("textarea");
tempInput.style = "position: absolute; left: -1000px; top: -1000px;";
const tempInput = document.createElement('textarea');
tempInput.style = 'position: absolute; left: -1000px; top: -1000px;';
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
document.execCommand('copy');
document.body.removeChild(tempInput);
}
export default Clipboard;

View File

@ -1,4 +1,4 @@
import {kebabToCamel, camelToKebab} from "./text";
import {kebabToCamel, camelToKebab} from './text';
/**
* A mapping of active components keyed by name, with values being arrays of component
@ -19,44 +19,6 @@ const componentModelMap = {};
*/
const elementComponentMap = new WeakMap();
/**
* Initialize a component instance on the given dom element.
* @param {String} name
* @param {Element} element
*/
function initComponent(name, element) {
/** @type {Function<Component>|undefined} **/
const componentModel = componentModelMap[name];
if (componentModel === undefined) return;
// Create our component instance
/** @type {Component} **/
let instance;
try {
instance = new componentModel();
instance.$name = name;
instance.$el = element;
const allRefs = parseRefs(name, element);
instance.$refs = allRefs.refs;
instance.$manyRefs = allRefs.manyRefs;
instance.$opts = parseOpts(name, element);
instance.setup();
} catch (e) {
console.error('Failed to create component', e, name, element);
}
// Add to global listing
if (typeof components[name] === "undefined") {
components[name] = [];
}
components[name].push(instance);
// Add to element mapping
const elComponents = elementComponentMap.get(element) || {};
elComponents[name] = instance;
elementComponentMap.set(element, elComponents);
}
/**
* Parse out the element references within the given element
* for the given component name.
@ -67,7 +29,7 @@ function parseRefs(name, element) {
const refs = {};
const manyRefs = {};
const prefix = `${name}@`
const prefix = `${name}@`;
const selector = `[refs*="${prefix}"]`;
const refElems = [...element.querySelectorAll(selector)];
if (element.matches(selector)) {
@ -93,13 +55,13 @@ function parseRefs(name, element) {
/**
* Parse out the element component options.
* @param {String} name
* @param {String} componentName
* @param {Element} element
* @return {Object<String, String>}
*/
function parseOpts(name, element) {
function parseOpts(componentName, element) {
const opts = {};
const prefix = `option:${name}:`;
const prefix = `option:${componentName}:`;
for (const {name, value} of element.attributes) {
if (name.startsWith(prefix)) {
const optName = name.replace(prefix, '');
@ -109,12 +71,50 @@ function parseOpts(name, element) {
return opts;
}
/**
* Initialize a component instance on the given dom element.
* @param {String} name
* @param {Element} element
*/
function initComponent(name, element) {
/** @type {Function<Component>|undefined} * */
const ComponentModel = componentModelMap[name];
if (ComponentModel === undefined) return;
// Create our component instance
/** @type {Component} * */
let instance;
try {
instance = new ComponentModel();
instance.$name = name;
instance.$el = element;
const allRefs = parseRefs(name, element);
instance.$refs = allRefs.refs;
instance.$manyRefs = allRefs.manyRefs;
instance.$opts = parseOpts(name, element);
instance.setup();
} catch (e) {
console.error('Failed to create component', e, name, element);
}
// Add to global listing
if (typeof components[name] === 'undefined') {
components[name] = [];
}
components[name].push(instance);
// Add to element mapping
const elComponents = elementComponentMap.get(element) || {};
elComponents[name] = instance;
elementComponentMap.set(element, elComponents);
}
/**
* Initialize all components found within the given element.
* @param {Element|Document} parentElement
*/
export function init(parentElement = document) {
const componentElems = parentElement.querySelectorAll(`[component],[components]`);
const componentElems = parentElement.querySelectorAll('[component],[components]');
for (const el of componentElems) {
const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
@ -162,4 +162,4 @@ export function get(name) {
export function firstOnElement(element, name) {
const elComponents = elementComponentMap.get(element) || {};
return elComponents[name] || null;
}
}

View File

@ -1,24 +1,23 @@
export function getCurrentDay() {
let date = new Date();
let month = date.getMonth() + 1;
let day = date.getDate();
const date = new Date();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${date.getFullYear()}-${(month>9?'':'0') + month}-${(day>9?'':'0') + day}`;
return `${date.getFullYear()}-${(month > 9 ? '' : '0') + month}-${(day > 9 ? '' : '0') + day}`;
}
export function utcTimeStampToLocalTime(timestamp) {
let date = new Date(timestamp * 1000);
let hours = date.getHours();
let mins = date.getMinutes();
return `${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
const date = new Date(timestamp * 1000);
const hours = date.getHours();
const mins = date.getMinutes();
return `${(hours > 9 ? '' : '0') + hours}:${(mins > 9 ? '' : '0') + mins}`;
}
export function formatDateTime(date) {
let month = date.getMonth() + 1;
let day = date.getDate();
let hours = date.getHours();
let mins = date.getMinutes();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const mins = date.getMinutes();
return `${date.getFullYear()}-${(month>9?'':'0') + month}-${(day>9?'':'0') + day} ${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
}
return `${date.getFullYear()}-${(month > 9 ? '' : '0') + month}-${(day > 9 ? '' : '0') + day} ${(hours > 9 ? '' : '0') + hours}:${(mins > 9 ? '' : '0') + mins}`;
}

View File

@ -5,7 +5,7 @@
*/
export function forEach(selector, callback) {
const elements = document.querySelectorAll(selector);
for (let element of elements) {
for (const element of elements) {
callback(element);
}
}
@ -17,7 +17,7 @@ export function forEach(selector, callback) {
* @param {Function<Event>} callback
*/
export function onEvents(listenerElement, events, callback) {
for (let eventName of events) {
for (const eventName of events) {
listenerElement.addEventListener(eventName, callback);
}
}
@ -35,7 +35,7 @@ export function onSelect(elements, callback) {
for (const listenerElement of elements) {
listenerElement.addEventListener('click', callback);
listenerElement.addEventListener('keydown', (event) => {
listenerElement.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
callback(event);
@ -58,7 +58,7 @@ export function onEnterPress(elements, callback) {
if (event.key === 'Enter') {
callback(event);
}
}
};
elements.forEach(e => e.addEventListener('keypress', listener));
}
@ -73,7 +73,7 @@ export function onEnterPress(elements, callback) {
* @param {Function} callback
*/
export function onChildEvent(listenerElement, childSelector, eventName, callback) {
listenerElement.addEventListener(eventName, function(event) {
listenerElement.addEventListener(eventName, event => {
const matchingChild = event.target.closest(childSelector);
if (matchingChild) {
callback.call(matchingChild, event, matchingChild);
@ -91,7 +91,7 @@ export function onChildEvent(listenerElement, childSelector, eventName, callback
export function findText(selector, text) {
const elements = document.querySelectorAll(selector);
text = text.toLowerCase();
for (let element of elements) {
for (const element of elements) {
if (element.textContent.toLowerCase().includes(text)) {
return element;
}
@ -105,7 +105,7 @@ export function findText(selector, text) {
* @param {Element} element
*/
export function showLoading(element) {
element.innerHTML = `<div class="loading-container"><div></div><div></div><div></div></div>`;
element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
}
/**
@ -130,4 +130,4 @@ export function htmlToDom(html) {
wrap.innerHTML = html;
window.$components.init(wrap);
return wrap.children[0];
}
}

View File

@ -1,29 +1,41 @@
let iFrame = null;
let lastApprovedOrigin;
let onInit, onSave;
let onInit; let
onSave;
/**
* Show the draw.io editor.
* @param {String} drawioUrl
* @param {Function} onInitCallback - Must return a promise with the xml to load for the editor.
* @param {Function} onSaveCallback - Is called with the drawing data on save.
*/
function show(drawioUrl, onInitCallback, onSaveCallback) {
onInit = onInitCallback;
onSave = onSaveCallback;
iFrame = document.createElement('iframe');
iFrame.setAttribute('frameborder', '0');
window.addEventListener('message', drawReceive);
iFrame.setAttribute('src', drawioUrl);
iFrame.setAttribute('class', 'fullscreen');
iFrame.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iFrame);
lastApprovedOrigin = (new URL(drawioUrl)).origin;
function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
}
function close() {
drawEventClose();
function drawEventExport(message) {
if (onSave) {
onSave(message.data);
}
}
function drawEventSave(message) {
drawPostMessage({
action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing',
});
}
function drawEventInit() {
if (!onInit) return;
onInit().then(xml => {
drawPostMessage({action: 'load', autosave: 1, xml});
});
}
function drawEventConfigure() {
const config = {};
window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});
drawPostMessage({action: 'configure', config});
}
function drawEventClose() {
// eslint-disable-next-line no-use-before-define
window.removeEventListener('message', drawReceive);
if (iFrame) document.body.removeChild(iFrame);
}
/**
@ -48,53 +60,45 @@ function drawReceive(event) {
}
}
function drawEventExport(message) {
if (onSave) {
onSave(message.data);
}
/**
* Show the draw.io editor.
* @param {String} drawioUrl
* @param {Function} onInitCallback - Must return a promise with the xml to load for the editor.
* @param {Function} onSaveCallback - Is called with the drawing data on save.
*/
export function show(drawioUrl, onInitCallback, onSaveCallback) {
onInit = onInitCallback;
onSave = onSaveCallback;
iFrame = document.createElement('iframe');
iFrame.setAttribute('frameborder', '0');
window.addEventListener('message', drawReceive);
iFrame.setAttribute('src', drawioUrl);
iFrame.setAttribute('class', 'fullscreen');
iFrame.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iFrame);
lastApprovedOrigin = (new URL(drawioUrl)).origin;
}
function drawEventSave(message) {
drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
}
function drawEventInit() {
if (!onInit) return;
onInit().then(xml => {
drawPostMessage({action: 'load', autosave: 1, xml: xml});
});
}
function drawEventConfigure() {
const config = {};
window.$events.emitPublic(iFrame, 'editor-drawio::configure', {config});
drawPostMessage({action: 'configure', config});
}
function drawEventClose() {
window.removeEventListener('message', drawReceive);
if (iFrame) document.body.removeChild(iFrame);
}
function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
}
async function upload(imageData, pageUploadedToId) {
let data = {
export async function upload(imageData, pageUploadedToId) {
const data = {
image: imageData,
uploaded_to: pageUploadedToId,
};
const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data);
const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);
return resp.data;
}
export function close() {
drawEventClose();
}
/**
* Load an existing image, by fetching it as Base64 from the system.
* @param drawingId
* @returns {Promise<string>}
*/
async function load(drawingId) {
export async function load(drawingId) {
try {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
@ -106,5 +110,3 @@ async function load(drawingId) {
throw error;
}
}
export default {show, close, upload, load};

View File

@ -6,13 +6,12 @@ const stack = [];
* @param {String} eventName
* @param {*} eventData
*/
function emit(eventName, eventData) {
export function emit(eventName, eventData) {
stack.push({name: eventName, data: eventData});
if (typeof listeners[eventName] === 'undefined') return this;
let eventsToStart = listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) {
let event = eventsToStart[i];
event(eventData);
const listenersToRun = listeners[eventName] || [];
for (const listener of listenersToRun) {
listener(eventData);
}
}
@ -22,7 +21,7 @@ function emit(eventName, eventData) {
* @param {Function} callback
* @returns {Events}
*/
function listen(eventName, callback) {
export function listen(eventName, callback) {
if (typeof listeners[eventName] === 'undefined') listeners[eventName] = [];
listeners[eventName].push(callback);
}
@ -34,43 +33,49 @@ function listen(eventName, callback) {
* @param {String} eventName
* @param {Object} eventData
*/
function emitPublic(targetElement, eventName, eventData) {
export function emitPublic(targetElement, eventName, eventData) {
const event = new CustomEvent(eventName, {
detail: eventData,
bubbles: true
bubbles: true,
});
targetElement.dispatchEvent(event);
}
/**
* Notify of standard server-provided validation errors.
* @param {Object} error
* Emit a success event with the provided message.
* @param {String} message
*/
function showValidationErrors(error) {
if (!error.status) return;
if (error.status === 422 && error.data) {
const message = Object.values(error.data).flat().join('\n');
emit('error', message);
export function success(message) {
emit('success', message);
}
/**
* Emit an error event with the provided message.
* @param {String} message
*/
export function error(message) {
emit('error', message);
}
/**
* Notify of standard server-provided validation errors.
* @param {Object} responseErr
*/
export function showValidationErrors(responseErr) {
if (!responseErr.status) return;
if (responseErr.status === 422 && responseErr.data) {
const message = Object.values(responseErr.data).flat().join('\n');
error(message);
}
}
/**
* Notify standard server-provided error messages.
* @param {Object} error
* @param {Object} responseErr
*/
function showResponseError(error) {
if (!error.status) return;
if (error.status >= 400 && error.data && error.data.message) {
emit('error', error.data.message);
export function showResponseError(responseErr) {
if (!responseErr.status) return;
if (responseErr.status >= 400 && responseErr.data && responseErr.data.message) {
error(responseErr.data.message);
}
}
export default {
emit,
emitPublic,
listen,
success: (msg) => emit('success', msg),
error: (msg) => emit('error', msg),
showValidationErrors,
showResponseError,
}

View File

@ -1,56 +1,100 @@
/**
* @typedef FormattedResponse
* @property {Headers} headers
* @property {Response} original
* @property {Object|String} data
* @property {Boolean} redirected
* @property {Number} status
* @property {string} statusText
* @property {string} url
*/
/**
* 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}>}
* Get the content from a fetch response.
* Checks the content-type header to determine the format.
* @param {Response} response
* @returns {Promise<Object|String>}
*/
async function get(url, params = {}) {
return request(url, {
method: 'GET',
params,
});
async function getResponseContent(response) {
if (response.status === 204) {
return null;
}
const responseContentType = response.headers.get('Content-Type') || '';
const subType = responseContentType.split(';')[0].split('/').pop();
if (subType === 'javascript' || subType === 'json') {
return response.json();
}
return response.text();
}
export class HttpError extends Error {
constructor(response, content) {
super(response.statusText);
this.data = content;
this.headers = response.headers;
this.redirected = response.redirected;
this.status = response.status;
this.statusText = response.statusText;
this.url = response.url;
this.original = response;
}
}
/**
* Perform a HTTP POST request.
* 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} data
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
* @param {Object} options
* @returns {Promise<FormattedResponse>}
*/
async function post(url, data = null) {
return dataRequest('POST', url, data);
}
async function request(url, options = {}) {
let requestUrl = url;
/**
* 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);
}
if (!requestUrl.startsWith('http')) {
requestUrl = window.baseUrl(requestUrl);
}
/**
* 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);
}
if (options.params) {
const urlObj = new URL(requestUrl);
for (const paramName of Object.keys(options.params)) {
const value = options.params[paramName];
if (typeof value !== 'undefined' && value !== null) {
urlObj.searchParams.set(paramName, value);
}
}
requestUrl = urlObj.toString();
}
/**
* 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);
const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
const requestOptions = {...options, credentials: 'same-origin'};
requestOptions.headers = {
...requestOptions.headers || {},
baseURL: window.baseUrl(''),
'X-CSRF-TOKEN': csrfToken,
};
const response = await fetch(requestUrl, requestOptions);
const content = await getResponseContent(response);
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw new HttpError(response, content);
}
return returnData;
}
/**
@ -59,11 +103,11 @@ async function performDelete(url, data = null) {
* @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}>}
* @returns {Promise<FormattedResponse>}
*/
async function dataRequest(method, url, data = null) {
const options = {
method: method,
method,
body: data,
};
@ -84,99 +128,61 @@ async function dataRequest(method, url, data = null) {
options.method = 'post';
}
return request(url, options)
return request(url, options);
}
/**
* Create a new HTTP request, setting the required CSRF information
* to communicate with the back-end. Parses & formats the response.
* Perform a HTTP GET request.
* Can easily pass query parameters as the second parameter.
* @param {String} url
* @param {Object} options
* @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
* @param {Object} params
* @returns {Promise<FormattedResponse>}
*/
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',
export async function get(url, params = {}) {
return request(url, {
method: 'GET',
params,
});
options.headers = Object.assign({}, options.headers || {}, {
'baseURL': window.baseUrl(''),
'X-CSRF-TOKEN': csrfToken,
});
const response = await fetch(url, options);
const content = await getResponseContent(response);
const returnData = {
data: content,
headers: response.headers,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
url: response.url,
original: response,
};
if (!response.ok) {
throw new HttpError(response, content);
}
return returnData;
}
/**
* Get the content from a fetch response.
* Checks the content-type header to determine the format.
* @param {Response} response
* @returns {Promise<Object|String>}
* Perform a HTTP POST request.
* @param {String} url
* @param {Object} data
* @returns {Promise<FormattedResponse>}
*/
async function getResponseContent(response) {
if (response.status === 204) {
return null;
}
const responseContentType = response.headers.get('Content-Type') || '';
const subType = responseContentType.split(';')[0].split('/').pop();
if (subType === 'javascript' || subType === 'json') {
return await response.json();
}
return await response.text();
export async function post(url, data = null) {
return dataRequest('POST', url, data);
}
class HttpError extends Error {
constructor(response, content) {
super(response.statusText);
this.data = content;
this.headers = response.headers;
this.redirected = response.redirected;
this.status = response.status;
this.statusText = response.statusText;
this.url = response.url;
this.original = response;
}
/**
* Perform a HTTP PUT request.
* @param {String} url
* @param {Object} data
* @returns {Promise<FormattedResponse>}
*/
export async function put(url, data = null) {
return dataRequest('PUT', url, data);
}
export default {
get: get,
post: post,
put: put,
patch: patch,
delete: performDelete,
HttpError: HttpError,
};
/**
* Perform a HTTP PATCH request.
* @param {String} url
* @param {Object} data
* @returns {Promise<FormattedResponse>}
*/
export async function patch(url, data = null) {
return dataRequest('PATCH', url, data);
}
/**
* Perform a HTTP DELETE request.
* @param {String} url
* @param {Object} data
* @returns {Promise<FormattedResponse>}
*/
async function performDelete(url, data = null) {
return dataRequest('DELETE', url, data);
}
export {performDelete as delete};

View File

@ -57,7 +57,6 @@ export class KeyboardNavigationHandler {
* @param {KeyboardEvent} event
*/
#keydownHandler(event) {
// Ignore certain key events in inputs to allow text editing.
if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
return;
@ -72,7 +71,7 @@ export class KeyboardNavigationHandler {
} else if (event.key === 'Escape') {
if (this.onEscape) {
this.onEscape(event);
} else if (document.activeElement) {
} else if (document.activeElement) {
document.activeElement.blur();
}
} else if (event.key === 'Enter' && this.onEnter) {
@ -88,8 +87,9 @@ export class KeyboardNavigationHandler {
const focusable = [];
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
for (const container of this.containers) {
focusable.push(...container.querySelectorAll(selector))
focusable.push(...container.querySelectorAll(selector));
}
return focusable;
}
}
}

View File

@ -4,7 +4,7 @@
* @returns {string}
*/
export function kebabToCamel(kebab) {
const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1);
const words = kebab.split('-');
return words[0] + words.slice(1).map(ucFirst).join('');
}
@ -15,5 +15,5 @@ export function kebabToCamel(kebab) {
* @returns {String}
*/
export function camelToKebab(camelStr) {
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
}
return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
}

View File

@ -5,11 +5,7 @@
*/
class Translator {
/**
* Create an instance, Passing in the required translations
* @param translations
*/
constructor(translations) {
constructor() {
this.store = new Map();
this.parseTranslations();
}
@ -19,7 +15,7 @@ class Translator {
*/
parseTranslations() {
const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
for (let tag of translationMetaTags) {
for (const tag of translationMetaTags) {
const key = tag.getAttribute('key');
const value = tag.getAttribute('value');
this.store.set(key, value);
@ -27,7 +23,7 @@ class Translator {
}
/**
* Get a translation, Same format as laravel's 'trans' helper
* Get a translation, Same format as Laravel's 'trans' helper
* @param key
* @param replacements
* @returns {*}
@ -38,8 +34,8 @@ class Translator {
}
/**
* Get pluralised text, Dependant on the given count.
* Same format at laravel's 'trans_choice' helper.
* Get pluralised text, Dependent on the given count.
* Same format at Laravel's 'trans_choice' helper.
* @param key
* @param count
* @param replacements
@ -52,7 +48,7 @@ class Translator {
/**
* Parse the given translation and find the correct plural option
* to use. Similar format at laravel's 'trans_choice' helper.
* to use. Similar format at Laravel's 'trans_choice' helper.
* @param {String} translation
* @param {Number} count
* @param {Object} replacements
@ -64,7 +60,7 @@ class Translator {
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
let result = null;
for (let t of splitText) {
for (const t of splitText) {
// Parse exact matches
const exactMatches = t.match(exactCountRegex);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
@ -117,14 +113,17 @@ class Translator {
*/
performReplacements(string, replacements) {
if (!replacements) return string;
const replaceMatches = string.match(/:([\S]+)/g);
const replaceMatches = string.match(/:(\S+)/g);
if (replaceMatches === null) return string;
let updatedString = string;
replaceMatches.forEach(match => {
const key = match.substring(1);
if (typeof replacements[key] === 'undefined') return;
string = string.replace(match, replacements[key]);
updatedString = updatedString.replace(match, replacements[key]);
});
return string;
return updatedString;
}
}

View File

@ -1,5 +1,3 @@
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
@ -13,9 +11,9 @@
*/
export function debounce(func, wait, immediate) {
let timeout;
return function() {
const context = this, args = arguments;
const later = function() {
return function debouncedWrapper(...args) {
const context = this;
const later = function debouncedTimeout() {
timeout = null;
if (!immediate) func.apply(context, args);
};
@ -24,7 +22,7 @@ export function debounce(func, wait, immediate) {
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
}
/**
* Scroll and highlight an element.
@ -55,11 +53,11 @@ export function scrollAndHighlightElement(element) {
*/
export function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
@ -68,6 +66,7 @@ export function escapeHtml(unsafe) {
* @returns {string}
*/
export function uniqueId() {
const S4 = () => (((1+Math.random())*0x10000)|0).toString(16).substring(1);
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}
// eslint-disable-next-line no-bitwise
const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
}

View File

@ -1,8 +1,8 @@
import {
init,
attributesModule,
toVNode
} from "snabbdom";
toVNode,
} from 'snabbdom';
let patcher;
@ -12,7 +12,6 @@ let patcher;
function getPatcher() {
if (patcher) return patcher;
patcher = init([
attributesModule,
]);
@ -28,4 +27,4 @@ export function patchDomFromHtmlString(domTarget, html) {
const contentDom = document.createElement('div');
contentDom.innerHTML = html;
getPatcher()(toVNode(domTarget), toVNode(contentDom));
}
}

View File

@ -2,7 +2,6 @@
* @param {Editor} editor
*/
export function listen(editor) {
// Replace editor content
window.$events.listen('editor::replace', ({html}) => {
editor.setContent(html);
@ -31,4 +30,4 @@ export function listen(editor) {
editor.focus();
}
});
}
}

View File

@ -1,32 +1,35 @@
import {register as registerShortcuts} from "./shortcuts";
import {listen as listenForCommonEvents} from "./common-events";
import {scrollToQueryString} from "./scrolling";
import {listenForDragAndPaste} from "./drop-paste-handling";
import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars";
import {registerCustomIcons} from "./icons";
import {register as registerShortcuts} from './shortcuts';
import {listen as listenForCommonEvents} from './common-events';
import {scrollToQueryString} from './scrolling';
import {listenForDragAndPaste} from './drop-paste-handling';
import {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';
import {registerCustomIcons} from './icons';
import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
import {getPlugin as getAboutPlugin} from "./plugins-about";
import {getPlugin as getDetailsPlugin} from "./plugins-details";
import {getPlugin as getTasklistPlugin} from "./plugins-tasklist";
import {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';
import {getPlugin as getDrawioPlugin} from './plugin-drawio';
import {getPlugin as getCustomhrPlugin} from './plugins-customhr';
import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';
import {getPlugin as getAboutPlugin} from './plugins-about';
import {getPlugin as getDetailsPlugin} from './plugins-details';
import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
const style_formats = [
{title: "Large Header", format: "h2", preview: 'color: blue;'},
{title: "Medium Header", format: "h3"},
{title: "Small Header", format: "h4"},
{title: "Tiny Header", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
const styleFormats = [
{title: 'Large Header', format: 'h2', preview: 'color: blue;'},
{title: 'Medium Header', format: 'h3'},
{title: 'Small Header', format: 'h4'},
{title: 'Tiny Header', format: 'h5'},
{
title: "Callouts", items: [
{title: "Information", format: 'calloutinfo'},
{title: "Success", format: 'calloutsuccess'},
{title: "Warning", format: 'calloutwarning'},
{title: "Danger", format: 'calloutdanger'}
]
title: 'Paragraph', format: 'p', exact: true, classes: '',
},
{title: 'Blockquote', format: 'blockquote'},
{
title: 'Callouts',
items: [
{title: 'Information', format: 'calloutinfo'},
{title: 'Success', format: 'calloutsuccess'},
{title: 'Warning', format: 'calloutwarning'},
{title: 'Danger', format: 'calloutdanger'},
],
},
];
@ -37,10 +40,10 @@ const formats = {
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
};
const color_map = [
const colorMap = [
'#BFEDD2', '',
'#FBEEB8', '',
'#F8CAC6', '',
@ -66,14 +69,13 @@ const color_map = [
'#34495E', '',
'#000000', '',
'#ffffff', ''
'#ffffff', '',
];
function file_picker_callback(callback, value, meta) {
function filePickerCallback(callback, value, meta) {
// field_name, url, type, win
if (meta.filetype === 'file') {
/** @type {EntitySelectorPopup} **/
/** @type {EntitySelectorPopup} * */
const selector = window.$components.first('entity-selector-popup');
selector.show(entity => {
callback(entity.link, {
@ -85,13 +87,12 @@ function file_picker_callback(callback, value, meta) {
if (meta.filetype === 'image') {
// Show image manager
/** @type {ImageManager} **/
/** @type {ImageManager} * */
const imageManager = window.$components.first('image-manager');
imageManager.show(function (image) {
imageManager.show(image => {
callback(image.url, {alt: image.name});
}, 'gallery');
}
}
/**
@ -100,30 +101,30 @@ function file_picker_callback(callback, value, meta) {
*/
function gatherPlugins(options) {
const plugins = [
"image",
"table",
"link",
"autolink",
"fullscreen",
"code",
"customhr",
"autosave",
"lists",
"codeeditor",
"media",
"imagemanager",
"about",
"details",
"tasklist",
'image',
'table',
'link',
'autolink',
'fullscreen',
'code',
'customhr',
'autosave',
'lists',
'codeeditor',
'media',
'imagemanager',
'about',
'details',
'tasklist',
options.textDirection === 'rtl' ? 'directionality' : '',
];
window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
window.tinymce.PluginManager.add('about', getAboutPlugin(options));
window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
window.tinymce.PluginManager.add('about', getAboutPlugin());
window.tinymce.PluginManager.add('details', getDetailsPlugin());
window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
if (options.drawioUrl) {
window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@ -137,11 +138,11 @@ function gatherPlugins(options) {
* Fetch custom HTML head content from the parent page head into the editor.
*/
function fetchCustomHeadContent() {
const headContentLines = document.head.innerHTML.split("\n");
const headContentLines = document.head.innerHTML.split('\n');
const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
if (startLineIndex === -1 || endLineIndex === -1) {
return ''
return '';
}
return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
}
@ -152,10 +153,10 @@ function fetchCustomHeadContent() {
* @param {Editor} editor
*/
function setupBrFilter(editor) {
editor.serializer.addNodeFilter('br', function(nodes) {
editor.serializer.addNodeFilter('br', nodes => {
for (const node of nodes) {
if (node.parent && node.parent.name === 'code') {
const newline = tinymce.html.Node.create('#text');
const newline = window.tinymce.html.Node.create('#text');
newline.value = '\n';
node.replace(newline);
}
@ -168,7 +169,14 @@ function setupBrFilter(editor) {
* @return {function(Editor)}
*/
function getSetupCallback(options) {
return function(editor) {
return function setupCallback(editor) {
function editorChange() {
if (options.darkMode) {
editor.contentDocument.documentElement.classList.add('dark-mode');
}
window.$events.emit('editor-html-change', '');
}
editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
listenForCommonEvents(editor);
listenForDragAndPaste(editor, options);
@ -184,13 +192,6 @@ function getSetupCallback(options) {
setupBrFilter(editor);
});
function editorChange() {
if (options.darkMode) {
editor.contentDocument.documentElement.classList.add('dark-mode');
}
window.$events.emit('editor-html-change', '');
}
// Custom handler hook
window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
@ -200,9 +201,9 @@ function getSetupCallback(options) {
icon: 'sourcecode',
onAction() {
editor.execCommand('mceToggleFormat', false, 'code');
}
})
}
},
});
};
}
/**
@ -229,7 +230,6 @@ body {
* @return {Object}
*/
export function build(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);
@ -241,7 +241,7 @@ export function build(options) {
width: '100%',
height: '100%',
selector: '#html-editor',
cache_suffix: '?version=' + version,
cache_suffix: `?version=${version}`,
content_css: [
window.baseUrl('/dist/styles.css'),
],
@ -263,18 +263,18 @@ export function build(options) {
automatic_uploads: false,
custom_elements: 'doc-root,code-block',
valid_children: [
"-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
"+div[pre|img]",
"-doc-root[doc-root|#text]",
"-li[details]",
"+code-block[pre]",
"+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]"
'-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
'+div[pre|img]',
'-doc-root[doc-root|#text]',
'-li[details]',
'+code-block[pre]',
'+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]',
].join(','),
plugins: gatherPlugins(options),
contextmenu: false,
toolbar: getPrimaryToolbar(options),
content_style: getContentStyle(options),
style_formats,
style_formats: styleFormats,
style_formats_merge: false,
media_alt_source: false,
media_poster: false,
@ -282,10 +282,10 @@ export function build(options) {
table_style_by_css: true,
table_use_colgroups: true,
file_picker_types: 'file image',
color_map,
file_picker_callback,
color_map: colorMap,
file_picker_callback: filePickerCallback,
paste_preprocess(plugin, args) {
const content = args.content;
const {content} = args;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
@ -296,7 +296,7 @@ export function build(options) {
},
setup(editor) {
registerCustomIcons(editor);
registerAdditionalToolbars(editor, options);
registerAdditionalToolbars(editor);
getSetupCallback(options)(editor);
},
};
@ -312,4 +312,4 @@ export function build(options) {
* @property {int} pageId
* @property {Object} translations
* @property {Object} translationMap
*/
*/

View File

@ -1,4 +1,4 @@
import Clipboard from "../services/clipboard";
import {Clipboard} from '../services/clipboard';
let wrap;
let draggedContentEditable;
@ -7,6 +7,25 @@ function hasTextContent(node) {
return node && !!(node.textContent || node.innerText);
}
/**
* Upload an image file to the server
* @param {File} file
* @param {int} pageId
*/
async function uploadImageFile(file, pageId) {
if (file === null || file.type.indexOf('image') !== 0) {
throw new Error('Not an image file');
}
const remoteFilename = file.name || `image-${Date.now()}.png`;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', pageId);
const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
return resp.data;
}
/**
* Handle pasting images from clipboard.
* @param {Editor} editor
@ -23,8 +42,7 @@ function paste(editor, options, event) {
const images = clipboard.getImages();
for (const imageFile of images) {
const id = "image-" + Math.random().toString(16).slice(2);
const id = `image-${Math.random().toString(16).slice(2)}`;
const loadingImage = window.baseUrl('/loading.gif');
event.preventDefault();
@ -44,37 +62,17 @@ function paste(editor, options, event) {
}).catch(err => {
editor.dom.remove(id);
window.$events.emit('error', options.translations.imageUploadErrorText);
console.log(err);
console.error(err);
});
}, 10);
}
}
/**
* Upload an image file to the server
* @param {File} file
* @param {int} pageId
*/
async function uploadImageFile(file, pageId) {
if (file === null || file.type.indexOf('image') !== 0) {
throw new Error(`Not an image file`);
}
const remoteFilename = file.name || `image-${Date.now()}.png`;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', pageId);
const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
return resp.data;
}
/**
* @param {Editor} editor
* @param {WysiwygConfigOptions} options
*/
function dragStart(editor, options) {
let node = editor.selection.getNode();
function dragStart(editor) {
const node = editor.selection.getNode();
if (node.nodeName === 'IMG') {
wrap = editor.dom.getParent(node, '.mceTemp');
@ -96,8 +94,12 @@ function dragStart(editor, options) {
* @param {DragEvent} event
*/
function drop(editor, options, event) {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
const {dom} = editor;
const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
event.clientX,
event.clientY,
editor.getDoc(),
);
// Template insertion
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
@ -105,7 +107,7 @@ function drop(editor, options, event) {
event.preventDefault();
window.$http.get(`/templates/${templateId}`).then(resp => {
editor.selection.setRng(rng);
editor.undoManager.transact(function () {
editor.undoManager.transact(() => {
editor.execCommand('mceInsertContent', false, resp.data.html);
});
});
@ -117,7 +119,7 @@ function drop(editor, options, event) {
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.undoManager.transact(() => {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
@ -127,7 +129,7 @@ function drop(editor, options, event) {
// Handle contenteditable section drop
if (!event.isDefaultPrevented() && draggedContentEditable) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.undoManager.transact(() => {
const selectedNode = editor.selection.getNode();
const range = editor.selection.getRng();
const selectedNodeRoot = selectedNode.closest('body > *');
@ -152,7 +154,7 @@ function drop(editor, options, event) {
* @param {WysiwygConfigOptions} options
*/
export function listenForDragAndPaste(editor, options) {
editor.on('dragstart', () => dragStart(editor, options));
editor.on('drop', event => drop(editor, options, event));
editor.on('dragstart', () => dragStart(editor));
editor.on('drop', event => drop(editor, options, event));
editor.on('paste', event => paste(editor, options, event));
}
}

View File

@ -5,17 +5,15 @@ const icons = {
'table-insert-column-before': '<svg width="24" height="24"><path d="M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z"/></svg>',
'table-insert-row-above': '<svg width="24" height="24"><path d="M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z"/></svg>',
'table-insert-row-after': '<svg width="24" height="24"><path d="M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z"/></svg>',
'table': '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2ZM5 14v5h6v-5zm14 0h-6v5h6zm0-7h-6v5h6zM5 12h6V7H5Z"/></svg>',
table: '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2ZM5 14v5h6v-5zm14 0h-6v5h6zm0-7h-6v5h6zM5 12h6V7H5Z"/></svg>',
'table-delete-table': '<svg width="24" height="24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z"/><path d="m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z" style="fill-rule:nonzero;stroke-width:1.20992"/></svg>',
};
/**
* @param {Editor} editor
*/
export function registerCustomIcons(editor) {
for (const [name, svg] of Object.entries(icons)) {
editor.ui.registry.addIcon(name, svg);
}
}
}

View File

@ -10,8 +10,8 @@ function elemIsCodeBlock(elem) {
*/
function showPopup(editor, code, language, callback) {
window.$components.first('code-editor').open(code, language, (newCode, newLang) => {
callback(newCode, newLang)
editor.focus()
callback(newCode, newLang);
editor.focus();
});
}
@ -59,7 +59,7 @@ function defineCodeBlockCustomElement(editor) {
}
getLanguage() {
const getLanguageFromClassList = (classes) => {
const getLanguageFromClassList = classes => {
const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
return (langClasses[0] || '').replace('language-', '');
};
@ -114,12 +114,14 @@ function defineCodeBlockCustomElement(editor) {
this.style.height = `${height}px`;
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
const renderEditor = (Code) => {
const renderEditor = Code => {
this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
setTimeout(() => this.style.height = null, 12);
setTimeout(() => {
this.style.height = null;
}, 12);
};
window.importVersioned('code').then((Code) => {
window.importVersioned('code').then(Code => {
const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
setTimeout(() => renderEditor(Code), timeout);
});
@ -135,26 +137,24 @@ function defineCodeBlockCustomElement(editor) {
}
}
}
}
win.customElements.define('code-block', CodeBlockElement);
}
/**
* @param {Editor} editor
* @param {String} url
*/
function register(editor, url) {
editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>')
function register(editor) {
editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>');
editor.ui.registry.addButton('codeeditor', {
tooltip: 'Insert code block',
icon: 'codeblock',
onAction() {
editor.execCommand('codeeditor');
}
},
});
editor.ui.registry.addButton('editcodeeditor', {
@ -162,7 +162,7 @@ function register(editor, url) {
icon: 'edit-block',
onAction() {
editor.execCommand('codeeditor');
}
},
});
editor.addCommand('codeeditor', () => {
@ -184,17 +184,17 @@ function register(editor, url) {
}
});
editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode();
editor.on('dblclick', () => {
const selectedNode = editor.selection.getNode();
if (elemIsCodeBlock(selectedNode)) {
showPopupForCodeBlock(editor, selectedNode);
}
});
editor.on('PreInit', () => {
editor.parser.addNodeFilter('pre', function(elms) {
editor.parser.addNodeFilter('pre', elms => {
for (const el of elms) {
const wrapper = tinymce.html.Node.create('code-block', {
const wrapper = window.tinymce.html.Node.create('code-block', {
contenteditable: 'false',
});
@ -207,13 +207,13 @@ function register(editor, url) {
}
});
editor.parser.addNodeFilter('code-block', function(elms) {
editor.parser.addNodeFilter('code-block', elms => {
for (const el of elms) {
el.attr('contenteditable', 'false');
}
});
editor.serializer.addNodeFilter('code-block', function(elms) {
editor.serializer.addNodeFilter('code-block', elms => {
for (const el of elms) {
el.unwrap();
}
@ -221,12 +221,12 @@ function register(editor, url) {
});
editor.ui.registry.addContextToolbar('codeeditor', {
predicate: function (node) {
predicate(node) {
return node.nodeName.toLowerCase() === 'code-block';
},
items: 'editcodeeditor',
position: 'node',
scope: 'node'
scope: 'node',
});
editor.on('PreInit', () => {
@ -235,9 +235,8 @@ function register(editor, url) {
}
/**
* @param {WysiwygConfigOptions} options
* @return {register}
*/
export function getPlugin(options) {
export function getPlugin() {
return register;
}
}

View File

@ -1,4 +1,4 @@
import DrawIO from "../services/drawio";
import * as DrawIO from '../services/drawio';
let pageEditor = null;
let currentNode = null;
@ -16,12 +16,12 @@ function showDrawingManager(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
/** @type {ImageManager} **/
/** @type {ImageManager} * */
const imageManager = window.$components.first('image-manager');
imageManager.show(function (image) {
imageManager.show(image => {
if (selectedNode) {
const imgElem = selectedNode.querySelector('img');
pageEditor.undoManager.transact(function () {
pageEditor.undoManager.transact(() => {
pageEditor.dom.setAttrib(imgElem, 'src', image.url);
pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
});
@ -32,32 +32,26 @@ function showDrawingManager(mceEditor, selectedNode = null) {
}, 'drawio');
}
function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
DrawIO.show(options.drawioUrl, drawingInit, updateContent);
}
async function updateContent(pngData) {
const id = "image-" + Math.random().toString(16).slice(2);
const id = `image-${Math.random().toString(16).slice(2)}`;
const loadingImage = window.baseUrl('/loading.gif');
const handleUploadError = (error) => {
const handleUploadError = error => {
if (error.status === 413) {
window.$events.emit('error', options.translations.serverUploadLimitText);
} else {
window.$events.emit('error', options.translations.imageUploadErrorText);
}
console.log(error);
console.error(error);
};
// Handle updating an existing image
if (currentNode) {
DrawIO.close();
let imgElem = currentNode.querySelector('img');
const imgElem = currentNode.querySelector('img');
try {
const img = await DrawIO.upload(pngData, options.pageId);
pageEditor.undoManager.transact(function () {
pageEditor.undoManager.transact(() => {
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
});
@ -72,7 +66,7 @@ async function updateContent(pngData) {
DrawIO.close();
try {
const img = await DrawIO.upload(pngData, options.pageId);
pageEditor.undoManager.transact(function () {
pageEditor.undoManager.transact(() => {
pageEditor.dom.setAttrib(id, 'src', img.url);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
});
@ -83,7 +77,6 @@ async function updateContent(pngData) {
}, 5);
}
function drawingInit() {
if (!currentNode) {
return Promise.resolve('');
@ -93,6 +86,66 @@ function drawingInit() {
return DrawIO.load(drawingId);
}
function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
DrawIO.show(options.drawioUrl, drawingInit, updateContent);
}
/**
* @param {Editor} editor
*/
function register(editor) {
editor.addCommand('drawio', () => {
const selectedNode = editor.selection.getNode();
showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
});
editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`);
editor.ui.registry.addSplitButton('drawio', {
tooltip: 'Insert/edit drawing',
icon: 'diagram',
onAction() {
editor.execCommand('drawio');
// Hack to de-focus the tinymce editor toolbar
window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
},
fetch(callback) {
callback([
{
type: 'choiceitem',
text: 'Drawing manager',
value: 'drawing-manager',
},
]);
},
onItemAction(api, value) {
if (value === 'drawing-manager') {
const selectedNode = editor.selection.getNode();
showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
}
},
});
editor.on('dblclick', () => {
const selectedNode = editor.selection.getNode();
if (!isDrawing(selectedNode)) return;
showDrawingEditor(editor, selectedNode);
});
editor.on('SetContent', () => {
const drawings = editor.dom.select('body > div[drawio-diagram]');
if (!drawings.length) return;
editor.undoManager.transact(() => {
for (const drawing of drawings) {
drawing.setAttribute('contenteditable', 'false');
}
});
});
}
/**
*
* @param {WysiwygConfigOptions} providedOptions
@ -100,56 +153,5 @@ function drawingInit() {
*/
export function getPlugin(providedOptions) {
options = providedOptions;
return function(editor, url) {
editor.addCommand('drawio', () => {
const selectedNode = editor.selection.getNode();
showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
});
editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`)
editor.ui.registry.addSplitButton('drawio', {
tooltip: 'Insert/edit drawing',
icon: 'diagram',
onAction() {
editor.execCommand('drawio');
// Hack to de-focus the tinymce editor toolbar
window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
},
fetch(callback) {
callback([
{
type: 'choiceitem',
text: 'Drawing manager',
value: 'drawing-manager',
}
]);
},
onItemAction(api, value) {
if (value === 'drawing-manager') {
const selectedNode = editor.selection.getNode();
showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
}
}
});
editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode();
if (!isDrawing(selectedNode)) return;
showDrawingEditor(editor, selectedNode);
});
editor.on('SetContent', function () {
const drawings = editor.dom.select('body > div[drawio-diagram]');
if (!drawings.length) return;
editor.undoManager.transact(function () {
for (const drawing of drawings) {
drawing.setAttribute('contenteditable', 'false');
}
});
});
};
}
return register;
}

Some files were not shown because too many files have changed in this diff Show More