Merge pull request #4181 from BookStackApp/js_formatting
Added standard JS formatting via ESLint
This commit is contained in:
		
						commit
						9f467f4052
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										88
									
								
								package.json
								
								
								
								
							
							
						
						
									
										88
									
								
								package.json
								
								
								
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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});
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,7 +122,7 @@ export function getTheme(viewParentEl) {
 | 
			
		|||
            if (tagStyles.length) {
 | 
			
		||||
                highlightStyle = HighlightStyle.define(tagStyles);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 || []),
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {Component} from "./component";
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class AutoSubmit extends Component {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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('');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,7 +8,7 @@ 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);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,12 +2,12 @@
 | 
			
		|||
 * 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);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,7 +42,7 @@ export class ConfirmDialog extends Component {
 | 
			
		|||
     */
 | 
			
		||||
    sendResult(result) {
 | 
			
		||||
        if (this.res) {
 | 
			
		||||
            this.res(result)
 | 
			
		||||
            this.res(result);
 | 
			
		||||
            this.res = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {Component} from "./component";
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class CustomCheckbox extends Component {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {onSelect} from "../services/dom";
 | 
			
		||||
import {Component} from "./component";
 | 
			
		||||
import {onSelect} from '../services/dom';
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * EventEmitSelect
 | 
			
		||||
| 
						 | 
				
			
			@ -12,12 +12,12 @@ 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);
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,7 +35,7 @@ export class ExpandToggle extends Component {
 | 
			
		|||
 | 
			
		||||
    updateSystemAjax(isOpen) {
 | 
			
		||||
        window.$http.patch(this.updateEndpoint, {
 | 
			
		||||
            expand: isOpen ? 'true' : 'false'
 | 
			
		||||
            expand: isOpen ? 'true' : 'false',
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -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 = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}%`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {Component} from "./component";
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class NewUserPassword extends Component {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,7 +190,7 @@ 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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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()]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,7 +57,7 @@ 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'));
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)}`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {Component} from "./component";
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tabs
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import {Component} from "./component";
 | 
			
		||||
import {Component} from './component';
 | 
			
		||||
 | 
			
		||||
export class ToggleSwitch extends Component {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,7 +20,7 @@ export class UserSelect extends Component {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    hide() {
 | 
			
		||||
        /** @var {Dropdown} **/
 | 
			
		||||
        /** @var {Dropdown} * */
 | 
			
		||||
        const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
 | 
			
		||||
        dropdown.hide();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,7 +45,7 @@ export class WysiwygEditor extends Component {
 | 
			
		|||
     */
 | 
			
		||||
    getContent() {
 | 
			
		||||
        return {
 | 
			
		||||
            html: this.editor.getContent()
 | 
			
		||||
            html: this.editor.getContent(),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = "[](" + image.url + ")";
 | 
			
		||||
            const newText = `[](${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 = ``;
 | 
			
		||||
        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,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}`;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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};
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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());
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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, "&")
 | 
			
		||||
        .replace(/</g, "<")
 | 
			
		||||
        .replace(/>/g, ">")
 | 
			
		||||
        .replace(/"/g, """)
 | 
			
		||||
        .replace(/'/g, "'");
 | 
			
		||||
        .replace(/&/g, '&')
 | 
			
		||||
        .replace(/</g, '<')
 | 
			
		||||
        .replace(/>/g, '>')
 | 
			
		||||
        .replace(/"/g, '"')
 | 
			
		||||
        .replace(/'/g, ''');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -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()}`);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    ]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@
 | 
			
		|||
 * @param {Editor} editor
 | 
			
		||||
 */
 | 
			
		||||
export function listen(editor) {
 | 
			
		||||
 | 
			
		||||
    // Replace editor content
 | 
			
		||||
    window.$events.listen('editor::replace', ({html}) => {
 | 
			
		||||
        editor.setContent(html);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,16 +5,14 @@ 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);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
		Loading…
	
		Reference in New Issue