Merge branch 'turbo'

This commit is contained in:
Jan Böhmer 2022-08-04 00:18:29 +02:00
commit 4c4b610daa
195 changed files with 7485 additions and 6769 deletions

View file

@ -2,9 +2,13 @@ name: Static analysis
on:
push:
branches: [ master ]
branches:
- '*'
- "!i10n_*" # Dont test localization branches
pull_request:
branches: [ master ]
branches:
- '*'
- "!i10n_*"
jobs:
phpstan:

View file

@ -94,7 +94,7 @@ jobs:
run: yarn install
- name: Build frontend
run: yarn dev
run: yarn build
- name: Create DB
run: php bin/console --env test doctrine:database:create --if-not-exists -n

View file

@ -26,10 +26,10 @@ RUN docker-php-ext-enable opcache; \
echo 'realpath_cache_ttl=600'; \
} > /usr/local/etc/php/conf.d/symfony-recommended.ini
# Install yarn
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && apt-get update && apt-get install -y nodejs yarn && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
@ -48,7 +48,6 @@ RUN a2enmod rewrite
USER www-data
RUN composer install -a --no-dev && composer clear-cache
RUN yarn install && yarn build && yarn cache clean
RUN php bin/console --env=prod ckeditor:install --clear=skip
# Use demo env to output logs to stdout
ENV APP_ENV=demo

View file

@ -1,12 +0,0 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
// start the Stimulus application
import './bootstrap';

View file

@ -0,0 +1,220 @@
/**
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
import Image from '@ckeditor/ckeditor5-image/src/image.js';
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import Link from '@ckeditor/ckeditor5-link/src/link.js';
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import List from '@ckeditor/ckeditor5-list/src/list.js';
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import Table from '@ckeditor/ckeditor5-table/src/table.js';
import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import PartDBLabel from "./plugins/PartDBLabel/PartDBLabel";
class Editor extends ClassicEditor {}
// Plugins to include in the build.
Editor.builtinPlugins = [
Alignment,
Autoformat,
Base64UploadAdapter,
BlockQuote,
Bold,
Code,
CodeBlock,
Essentials,
FindAndReplace,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
GeneralHtmlSupport,
Heading,
Highlight,
HorizontalLine,
HtmlComment,
HtmlEmbed,
Image,
ImageResize,
ImageStyle,
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
LinkImage,
List,
ListProperties,
MediaEmbed,
MediaEmbedToolbar,
Paragraph,
PasteFromOffice,
RemoveFormat,
SourceEditing,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableCaption,
TableCellProperties,
TableColumnResize,
TableProperties,
TableToolbar,
Underline,
WordCount,
PartDBLabel
];
// Editor configuration.
Editor.defaultConfig = {
toolbar: {
items: [
'heading',
'alignment',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'removeFormat',
'highlight',
'|',
'fontBackgroundColor',
'fontColor',
'fontSize',
'|',
'fontFamily',
'link',
'bulletedList',
'numberedList',
'outdent',
'indent',
'|',
'specialCharacters',
'horizontalLine',
'|',
'imageUpload',
'blockQuote',
'insertTable',
'mediaEmbed',
'code',
'codeBlock',
'htmlEmbed',
'|',
'undo',
'redo',
'findAndReplace',
'sourceEditing',
'|',
'partdb_label',
],
shouldNotGroupWhenFull: true
},
language: 'en',
fontFamily: {
options: [
'default',
'DejaVu Sans Mono, monospace',
'DejaVu Sans, sans-serif',
'DejaVu Serif, serif',
'Helvetica, Arial, sans-serif',
'Times New Roman, Times, serif',
'Courier New, Courier, monospace'
],
supportAllValues: true
},
'fontSize': {
options: [
8,
11,
13,
'default',
17,
19,
21,
],
supportAllValues: true
},
image: {
toolbar: [
'imageTextAlternative',
'imageStyle:inline',
'imageStyle:block',
'imageStyle:side',
'linkImage'
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'tableCellProperties',
'tableProperties'
]
}
};
export default { Editor, EditorWatchdog };

View file

@ -0,0 +1,194 @@
/**
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter.js';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import HtmlComment from '@ckeditor/ckeditor5-html-support/src/htmlcomment.js';
import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed.js';
import Image from '@ckeditor/ckeditor5-image/src/image.js';
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import Link from '@ckeditor/ckeditor5-link/src/link.js';
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import List from '@ckeditor/ckeditor5-list/src/list.js';
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
import Markdown from '@ckeditor/ckeditor5-markdown-gfm/src/markdown.js';
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import Table from '@ckeditor/ckeditor5-table/src/table.js';
import TableCaption from '@ckeditor/ckeditor5-table/src/tablecaption.js';
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import WordCount from '@ckeditor/ckeditor5-word-count/src/wordcount.js';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import TodoList from '@ckeditor/ckeditor5-list/src/todolist';
import ExtendedMarkdown from "./plugins/extendedMarkdown.js";
import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
class Editor extends ClassicEditor {}
// Plugins to include in the build.
Editor.builtinPlugins = [
Autoformat,
Base64UploadAdapter,
BlockQuote,
Bold,
Code,
CodeBlock,
Essentials,
FindAndReplace,
FontBackgroundColor,
FontColor,
FontSize,
GeneralHtmlSupport,
Heading,
Highlight,
HorizontalLine,
Image,
ImageResize,
ImageStyle,
ImageToolbar,
ImageUpload,
Indent,
IndentBlock,
Italic,
Link,
LinkImage,
List,
//MediaEmbed,
//MediaEmbedToolbar,
Paragraph,
PasteFromOffice,
RemoveFormat,
SourceEditing,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Table,
TableProperties,
TableToolbar,
Underline,
TodoList,
//Our own extensions
ExtendedMarkdown,
SpecialCharactersEmoji
];
// Editor configuration.
Editor.defaultConfig = {
toolbar: {
items: [
'heading',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'removeFormat',
'highlight',
'|',
'fontBackgroundColor',
'fontColor',
'fontSize',
'|',
'link',
'bulletedList',
'numberedList',
'outdent',
'indent',
'|',
'specialCharacters',
'horizontalLine',
'|',
'imageUpload',
'blockQuote',
'insertTable',
//'mediaEmbed',
'code',
'codeBlock',
'todoList',
'|',
'undo',
'redo',
'findAndReplace',
'sourceEditing',
],
shouldNotGroupWhenFull: true
},
language: 'en',
image: {
toolbar: [
'imageTextAlternative',
'imageStyle:inline',
'imageStyle:block',
'imageStyle:side',
'linkImage'
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'tableProperties'
]
},
list: {
properties: {
styles: false,
}
}
};
export default { Editor, EditorWatchdog };

View file

@ -0,0 +1,94 @@
/**
* @license Copyright (c) 2014-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import Code from '@ckeditor/ckeditor5-basic-styles/src/code.js';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import Link from '@ckeditor/ckeditor5-link/src/link.js';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog.js';
import ExtendedMarkdownInline from "./plugins/extendedMarkdownInline";
import SingleLinePlugin from "./plugins/singleLine";
import SpecialCharactersEmoji from "./plugins/special_characters_emoji";
class Editor extends ClassicEditor {}
// Plugins to include in the build.
Editor.builtinPlugins = [
Autoformat,
AutoLink,
Bold,
Code,
FindAndReplace,
Highlight,
Italic,
Link,
Paragraph,
RemoveFormat,
SourceEditing,
SpecialCharacters,
SpecialCharactersArrows,
SpecialCharactersCurrency,
SpecialCharactersEssentials,
SpecialCharactersLatin,
SpecialCharactersMathematical,
SpecialCharactersText,
Strikethrough,
Subscript,
Superscript,
Underline,
Essentials,
ExtendedMarkdownInline,
SingleLinePlugin,
SpecialCharactersEmoji
];
// Editor configuration.
Editor.defaultConfig = {
toolbar: {
items: [
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'removeFormat',
'highlight',
'|',
'link',
'code',
'specialCharacters',
'|',
'undo',
'redo',
'findAndReplace',
'sourceEditing'
]
},
language: 'en'
};
export default { Editor, EditorWatchdog };

View file

@ -0,0 +1,22 @@
.cke_placeholder {
background: #ffff00;
padding: 4px 2px;
outline-offset: -2px;
line-height: 1em;
margin: 0 1px;
}
.cke_placeholder::selection {
display: none;
}
/*.cke_overflow_dropdown {
overflow: auto;
height: 40vh;
}*/
/** Make long editor dropdown panels scrollable */
.ck-dropdown__panel {
overflow: auto;
max-height: 40vh;
}

View file

@ -0,0 +1,16 @@
import PartDBLabelUI from "./PartDBLabelUI";
import PartDBLabelEditing from "./PartDBLabelEditing";
import "./PartDBLabel.css";
import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
export default class PartDBLabel extends Plugin {
static get requires() {
return [PartDBLabelUI, PartDBLabelEditing];
}
static get pluginName() {
return 'PartDBLabel';
}
}

View file

@ -0,0 +1,31 @@
import Command from '@ckeditor/ckeditor5-core/src/command';
export default class PartDBLabelCommand extends Command {
execute( { value } ) {
const editor = this.editor;
const selection = editor.model.document.selection;
editor.model.change( writer => {
// Create a <placeholder> elment with the "name" attribute (and all the selection attributes)...
const placeholder = writer.createElement( 'partdb_label', {
...Object.fromEntries( selection.getAttributes() ),
name: value
} );
// ... and insert it into the document.
editor.model.insertContent( placeholder );
// Put the selection on the inserted element.
writer.setSelection( placeholder, 'on' );
} );
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const isAllowed = model.schema.checkChild( selection.focus.parent, 'partdb_label' );
this.isEnabled = isAllowed;
}
}

View file

@ -0,0 +1,86 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import PartDBLabelCommand from "./PartDBLabelCommand";
import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
export default class PartDBLabelEditing extends Plugin {
static get requires() { // ADDED
return [ Widget ];
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add('partdb_label', new PartDBLabelCommand( this.editor ) );
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register('partdb_label', {
// Allow wherever text is allowed:
allowWhere: '$text',
// The placeholder will act as an inline node:
isInline: true,
// The inline widget is self-contained so it cannot be split by the caret and can be selected:
isObject: true,
allowAttributesOf: '$text',
allowAttributes: [ 'name' ]
});
}
_defineConverters() {
const conversion = this.editor.conversion;
conversion.for( 'upcast' ).elementToElement( {
view: {
name: 'span',
classes: [ 'cke_placeholder' ]
},
model: ( viewElement, { writer: modelWriter } ) => {
// Extract the "name" from "{name}".
const name = viewElement.getChild( 0 ).data;
return modelWriter.createElement( 'partdb_label', { name } );
}
} );
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'partdb_label',
view: ( modelItem, { writer: viewWriter } ) => {
const widgetElement = createPlaceholderView( modelItem, viewWriter );
// Enable widget handling on a placeholder element inside the editing view.
return toWidget( widgetElement, viewWriter );
}
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'partdb_label',
view: ( modelItem, { writer: viewWriter } ) => createPlaceholderView( modelItem, viewWriter )
} );
// Helper method for both downcast converters.
function createPlaceholderView( modelItem, viewWriter ) {
const name = modelItem.getAttribute( 'name' );
const placeholderView = viewWriter.createContainerElement( 'span', {
class: 'cke_placeholder'
} );
// Insert the placeholder name (as a text).
const innerText = viewWriter.createText( name );
viewWriter.insert( viewWriter.createPositionAt( placeholderView, 0 ), innerText );
return placeholderView;
}
}
}

View file

@ -0,0 +1,149 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
require('./lang/de.js');
import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import Model from '@ckeditor/ckeditor5-ui/src/model';
export default class PartDBLabelUI extends Plugin {
init() {
const editor = this.editor;
const t = editor.t;
// The "placeholder" dropdown must be registered among the UI components of the editor
// to be displayed in the toolbar.
editor.ui.componentFactory.add( 'partdb_label', locale => {
const dropdownView = createDropdown( locale );
// Populate the list in the dropdown with items.
addListToDropdown( dropdownView, getDropdownItemsDefinitions(t) );
dropdownView.buttonView.set( {
// The t() function helps localize the editor. All strings enclosed in t() can be
// translated and change when the language of the editor changes.
label: t( 'Label Placeholder' ),
tooltip: true,
withText: true
} );
// Disable the placeholder button when the command is disabled.
const command = editor.commands.get( 'partdb_label' );
dropdownView.bind( 'isEnabled' ).to( command );
// Execute the command when the dropdown item is clicked (executed).
this.listenTo( dropdownView, 'execute', evt => {
editor.execute( 'partdb_label', { value: evt.source.commandParam } );
editor.editing.view.focus();
} );
return dropdownView;
} );
}
}
const PLACEHOLDERS = [
{
label: 'Part',
entries: [
['[[ID]]', 'Database ID'],
['[[NAME]]', 'Part name'],
['[[CATEGORY]]', 'Category'],
['[[CATEGORY_FULL]]', 'Category (Full path)'],
['[[MANUFACTURER]]', 'Manufacturer'],
['[[MANUFACTURER_FULL]]', 'Manufacturer (Full path)'],
['[[FOOTPRINT]]', 'Footprint'],
['[[FOOTPRINT_FULL]]', 'Footprint (Full path)'],
['[[MASS]]', 'Mass'],
['[[MPN]]', 'Manufacturer Product Number (MPN)'],
['[[TAGS]]', 'Tags'],
['[[M_STATUS]]', 'Manufacturing status'],
['[[DESCRIPTION]]', 'Description'],
['[[DESCRIPTION_T]]', 'Description (plain text)'],
['[[COMMENT]]', 'Comment'],
['[[COMMENT_T]]', 'Comment (plain text)'],
['[[LAST_MODIFIED]]', 'Last modified datetime'],
['[[CREATION_DATE]]', 'Creation datetime'],
]
},
{
label: 'Part lot',
entries: [
['[[LOT_ID]]', 'Lot ID'],
['[[LOT_NAME]]', 'Lot name'],
['[[LOT_COMMENT]]', 'Lot comment'],
['[[EXPIRATION_DATE]]', 'Lot expiration date'],
['[[AMOUNT]]', 'Lot amount'],
['[[LOCATION]]', 'Storage location'],
['[[LOCATION_FULL]]', 'Storage location (Full path)'],
]
},
{
label: 'Storage location',
entries: [
['[[ID]]', 'Location ID'],
['[[NAME]]', 'Name'],
['[[FULL_PATH]]', 'Full path'],
['[[PARENT]]', 'Parent name'],
['[[PARENT_FULL_PATH]]', 'Parent full path'],
['[[COMMENT]]', 'Comment'],
['[[COMMENT_T]]', 'Comment (plain text)'],
['[[LAST_MODIFIED]]', 'Last modified datetime'],
['[[CREATION_DATE]]', 'Creation datetime'],
]
},
{
label: 'Globals',
entries: [
['[[USERNAME]]', 'Username'],
['[[USERNAME_FULL]]', 'Username (including name)'],
['[[DATETIME]]', 'Current datetime'],
['[[DATE]]', 'Current date'],
['[[TIME]]', 'Current time'],
['[[INSTALL_NAME]]', 'Instance name'],
['[[TYPE]]', 'Target type']
],
},
];
function getDropdownItemsDefinitions(t) {
const itemDefinitions = new Collection();
for ( const group of PLACEHOLDERS) {
//Add group header
itemDefinitions.add({
'type': 'separator',
model: new Model( {
withText: true,
})
});
itemDefinitions.add({
type: 'button',
model: new Model( {
label: t(group.label),
withText: true,
isEnabled: false,
} )
});
//Add group entries
for ( const entry of group.entries) {
const definition = {
type: 'button',
model: new Model( {
commandParam: entry[0],
label: t(entry[1]),
withText: true
} ),
};
// Add the item definition to the collection.
itemDefinitions.add( definition );
}
}
return itemDefinitions;
}

View file

@ -0,0 +1,54 @@
// Make sure that the global object is defined. If not, define it.
window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {};
// Make sure that the dictionary for Polish translations exist.
window.CKEDITOR_TRANSLATIONS[ 'de' ] = window.CKEDITOR_TRANSLATIONS[ 'de' ] || {};
window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary = window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary || {};
// Extend the dictionary for Polish translations with your translations:
Object.assign( window.CKEDITOR_TRANSLATIONS[ 'de' ].dictionary, {
'Label Placeholder': 'Label Platzhalter',
'Part': 'Bauteil',
'Database ID': 'Datenbank ID',
'Part name': 'Bauteilname',
'Category': 'Kategorie',
'Category (Full path)': 'Kategorie (Vollständiger Pfad)',
'Manufacturer': 'Hersteller',
'Manufacturer (Full path)': 'Hersteller (Vollständiger Pfad)',
'Footprint': 'Footprint',
'Footprint (Full path)': 'Footprint (Vollständiger Pfad)',
'Mass': 'Gewicht',
'Manufacturer Product Number (MPN)': 'Hersteller Produktnummer (MPN)',
'Tags': 'Tags',
'Manufacturing status': 'Herstellungsstatus',
'Description': 'Beschreibung',
'Description (plain text)': 'Beschreibung (Nur-Text)',
'Comment': 'Kommentar',
'Comment (plain text)': 'Kommentar (Nur-Text)',
'Last modified datetime': 'Zuletzt geändert',
'Creation datetime': 'Erstellt',
'Lot ID': 'Lot ID',
'Lot name': 'Lot Name',
'Lot comment': 'Lot Kommentar',
'Lot expiration date': 'Lot Ablaufdatum',
'Lot amount': 'Lot Menge',
'Storage location': 'Lagerort',
'Storage location (Full path)': 'Lagerort (Vollständiger Pfad)',
'Location ID': 'Lagerort ID',
'Name': 'Name',
'Full path': 'Vollständiger Pfad',
'Parent name': 'Name des Übergeordneten Elements',
'Parent full path': 'Ganzer Pfad des Übergeordneten Elements',
'Username': 'Benutzername',
'Username (including name)': 'Benutzername (inklusive Name)',
'Current datetime': 'Aktuelle Datum/Zeit',
'Current date': 'Aktuelles Datum',
'Current time': 'Aktuelle Zeit',
'Instance name': 'Instanzname',
'Target type': 'Zieltyp',
} );

View file

@ -0,0 +1,53 @@
import { Plugin } from 'ckeditor5/src/core';
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
const ALLOWED_TAGS = [
//Common elements
'sup',
'sub',
'u',
'kbd',
'mark',
'ins',
'small',
'abbr',
'br',
//Block elements
'span',
'p',
'img',
//These commands are somehow ignored: TODO
'left',
'center',
'right',
];
/**
* The GitHub Flavored Markdown (GFM) plugin with added HTML tags, which are kept in the output. (inline mode)
*
*/
export default class ExtendedMarkdown extends Plugin {
/**
* @inheritDoc
*/
constructor( editor ) {
super( editor );
editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
for (const tag of ALLOWED_TAGS) {
editor.data.processor.keepHtml(tag);
}
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'Markdown';
}
}

View file

@ -0,0 +1,42 @@
import { Plugin } from 'ckeditor5/src/core';
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
const ALLOWED_TAGS = [
//Common elements
'sup',
'sub',
'u',
'kbd',
'mark',
'ins',
'small',
'abbr',
'br',
'span',
];
/**
* The GitHub Flavored Markdown (GFM) plugin with added HTML tags, which are kept in the output. (inline mode)
*
*/
export default class ExtendedMarkdownInline extends Plugin {
/**
* @inheritDoc
*/
constructor( editor ) {
super( editor );
editor.data.processor = new GFMDataProcessor( editor.data.viewDocument );
for (const tag of ALLOWED_TAGS) {
editor.data.processor.keepHtml(tag);
}
}
/**
* @inheritDoc
*/
static get pluginName() {
return 'Markdown';
}
}

View file

@ -0,0 +1,29 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
export default class SingleLinePlugin extends Plugin {
init() {
const editor = this.editor;
const view = editor.editing.view;
const viewDocument = view.document;
//Listen to enter presses
this.listenTo( viewDocument, 'enter', ( evt, data ) => {
//If user presses enter, prevent the enter action
evt.stop();
}, { priority: 'high' } );
//And clipboard pastes
this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
let dataTransfer = data.dataTransfer;
//Clean text input (replace newlines with spaces)
let input = dataTransfer.getData("text");
let cleaned = input.replace(/\r?\n/g, ' ');
//We can not use the dataTransfer.setData method because the old object is somehow protected
data.dataTransfer = new DataTransfer();
data.dataTransfer.setData("text", cleaned);
}, { priority: 'high' } );
}
}

View file

@ -0,0 +1,26 @@
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
const emoji = require('emoji.json');
export default class SpecialCharactersEmoji extends Plugin {
init() {
const editor = this.editor;
const specialCharsPlugin = editor.plugins.get('SpecialCharacters');
specialCharsPlugin.addItems('Emoji', this.getEmojis());
}
getEmojis() {
//Map our emoji data to the format the plugin expects
return emoji.map(emoji => {
return {
title: emoji.name,
character: emoji.char
};
});
}
}

View file

@ -1,4 +1,11 @@
{
"controllers": [],
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View file

@ -0,0 +1,27 @@
import {Controller} from "@hotwired/stimulus";
import {Tooltip} from "bootstrap";
export default class extends Controller {
connect() {
window.addEventListener('scroll', this._onscroll.bind(this));
}
_onscroll() {
const button = this.element;
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
button.style.display = "block";
} else {
button.style.display = "none";
}
}
backToTop() {
//Hide button tooltip to prevent ugly tooltip on scroll
Tooltip.getInstance(this.element)?.hide();
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
}
}

View file

@ -0,0 +1,41 @@
import {Controller} from "@hotwired/stimulus";
import Darkmode from "darkmode-js/src";
import "darkmode-js"
export default class extends Controller {
_darkmode;
connect() {
if (typeof window.getComputedStyle(document.body).mixBlendMode == 'undefined') {
console.warn("The browser does not support mix blend mode. Darkmode will not work.");
return;
}
try {
const darkmode = new Darkmode();
this._darkmode = darkmode;
//Unhide darkmode button
this._showWidget();
//Set the switch according to our current darkmode state
const toggler = document.getElementById("toggleDarkmode");
toggler.checked = darkmode.isActivated();
}
catch (e)
{
console.error(e);
}
}
_showWidget() {
this.element.classList.remove('hidden');
}
toggleDarkmode() {
this._darkmode.toggle();
}
}

View file

@ -0,0 +1,73 @@
import {Controller} from "@hotwired/stimulus";
const STORAGE_KEY = 'hide_sidebar';
export default class extends Controller {
/**
* The element representing the sidebar which can be hidden.
* @type {HTMLElement}
* @private
*/
_sidebar;
/**
* The element of the container which is expanded to the full width.
* @type {HTMLElement}
* @private
*/
_container;
/**
* The button which toggles the sidebar.
* @private
*/
_toggle_button;
_hidden = false;
connect() {
this._sidebar = document.getElementById('fixed-sidebar');
this._container = document.getElementById('main');
this._toggle_button = this.element;
//Make the state persistent over reloads
if(localStorage.getItem(STORAGE_KEY) === 'true') {
sidebarHide();
}
}
hideSidebar() {
this._sidebar.classList.add('d-none');
this._container.classList.remove(...['col-md-9', 'col-lg-10', 'offset-md-3', 'offset-lg-2']);
this._container.classList.add('col-12');
//Change button icon
this._toggle_button.innerHTML = '<i class="fas fa-angle-right"></i>';
localStorage.setItem(STORAGE_KEY, 'true');
this._hidden = true;
}
showSidebar() {
this._sidebar.classList.remove('d-none');
this._container.classList.remove('col-12');
this._container.classList.add(...['col-md-9', 'col-lg-10', 'offset-md-3', 'offset-lg-2']);
//Change button icon
this._toggle_button.innerHTML = '<i class="fas fa-angle-left"></i>';
localStorage.setItem(STORAGE_KEY, 'false');
this._hidden = false;
}
toggleSidebar() {
if(this._hidden) {
this.showSidebar();
} else {
this.hideSidebar();
}
}
}

View file

@ -0,0 +1,36 @@
import {Controller} from "@hotwired/stimulus";
//import "katex";
import 'katex/dist/katex.css';
import {auto} from "@popperjs/core";
//import renderMathInElement from "katex/dist/contrib/auto-render";
export default class extends Controller {
connect() {
this.applyLatex();
this.element.addEventListener('markdown:finished', () => this.applyLatex());
}
applyLatex() {
//Only import the katex library, if we have an delimiter string in our element text
let str = this.element.textContent;
if(str.match(/(\$|\\\(|\\\[).+(\$|\\\)|\\\])/)) {
import('katex/dist/contrib/auto-render').then((autorender) => {
//This calls renderMathInElement()
autorender.default(this.element, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false},
{left: "\\[", right: "\\]", display: true}
]
});
})
}
}
mutate() {
this.applyLatex();
}
}

View file

@ -0,0 +1,69 @@
'use strict';
import { Controller } from '@hotwired/stimulus';
import { marked } from "marked";
import DOMPurify from 'dompurify';
import "../../css/markdown.css";
export default class extends Controller {
connect()
{
this.configureMarked();
this.render();
//Dispatch an event that we are now finished
const event = new CustomEvent('markdown:finished', {
bubbles: true
});
this.element.dispatchEvent(event);
}
render() {
let raw = this.element.dataset['markdown'];
//Apply purified parsed markdown
this.element.innerHTML = DOMPurify.sanitize(marked(this.unescapeHTML(raw)));
for(let a of this.element.querySelectorAll('a')) {
//Mark all links as external
a.classList.add('link-external');
//Open links in new tag
a.setAttribute('target', '_blank');
//Dont track
a.setAttribute('rel', 'noopener');
}
//Apply bootstrap styles to tables
for(let table of this.element.querySelectorAll('table')) {
table.classList.add('table', 'table-hover', 'table-striped', 'table-bordered', 'table-sm');
}
//Make header line dark
for(let head of this.element.querySelectorAll('thead')) {
head.classList.add('table-dark');
}
}
/**
* Unescape the given HTML
* @param {string} html
* @returns {string}
*/
unescapeHTML(html) {
var txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
}
/**
* Configure the marked parser
*/
configureMarked()
{
marked.setOptions({
gfm: true,
});
}
}

View file

@ -0,0 +1,17 @@
import { Controller } from '@hotwired/stimulus';
import { Toast } from 'bootstrap';
export default class extends Controller {
connect() {
//Move all toasts from the page into our toast container and show them
const toastContainer = document.getElementById('toast-container');
if (this.element.parentNode !== toastContainer) {
toastContainer.appendChild(this.element);
return;
}
const toast = new Toast(this.element);
toast.show();
}
}

View file

@ -0,0 +1,60 @@
import {Controller} from "@hotwired/stimulus";
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/tom-select_extensions.css';
import TomSelect from "tom-select";
export default class extends Controller {
_tomSelect;
connect() {
let settings = {
persistent: false,
create: true,
maxItems: 1,
createOnBlur: true,
render: {
item: (data, escape) => {
return '<span>' + escape(data.label) + '</span>';
},
option: (data, escape) => {
if (data.image) {
return "<div class='row m-0'><div class='col-2 pl-0 pr-1'><img class='typeahead-image' src='" + data.image + "'/></div><div class='col-10'>" + data.label + "</div></div>"
}
return '<div>' + escape(data.label) + '</div>';
}
}
};
if(this.element.dataset.autocomplete) {
const base_url = this.element.dataset.autocomplete;
settings.searchField = "label";
settings.sortField = "label";
settings.valueField = "label";
settings.load = (query, callback) => {
if(query.length < 2){
callback();
return;
}
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
fetch(url)
.then(response => response.json())
.then(json => {
const data = json.map(x => {
return {
"label": x.name,
"image": x.image,
}
});
callback(data);
}).catch(()=>{
callback();
});
};
}
this._tomSelect = new TomSelect(this.element, settings);
}
}

View file

@ -0,0 +1,29 @@
import {Controller} from "@hotwired/stimulus";
/**
* This controller synchronizes the filetype filters of the file input type with our selected attachment type
*/
export default class extends Controller
{
_selectInput;
_fileInput;
connect() {
//Find the select input for our attachment form
this._selectInput = this.element.querySelector('select');
//Find the file input for our attachment form
this._fileInput = this.element.querySelector('input[type="file"]');
this._selectInput.addEventListener('change', this.updateAllowedFiletypes.bind(this));
//Update file file on load
this.updateAllowedFiletypes();
}
updateAllowedFiletypes() {
let selected_option = this._selectInput.options[this._selectInput.selectedIndex];
let filetype_filter = selected_option.dataset.filetype_filter;
//Apply filetype filter to file input
this._fileInput.setAttribute('accept', filetype_filter);
}
}

View file

@ -0,0 +1,57 @@
import {Controller} from "@hotwired/stimulus";
import { default as FullEditor } from "../../ckeditor/markdown_full";
import { default as SingleLineEditor} from "../../ckeditor/markdown_single_line";
import { default as HTMLLabelEditor } from "../../ckeditor/html_label";
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
connect() {
const mode = this.element.dataset.mode;
let EDITOR_TYPE = "Invalid";
switch (mode) {
case "markdown-full":
EDITOR_TYPE = FullEditor['Editor'];
break;
case "markdown-single_line":
EDITOR_TYPE = SingleLineEditor['Editor'];
break;
case "html-label":
EDITOR_TYPE = HTMLLabelEditor['Editor'];
break;
default:
console.error("Unknown mode: " + mode);
return;
}
const language = document.body.dataset.locale ?? "en";
const config = {
language: language,
}
const watchdog = new EditorWatchdog();
watchdog.setCreator((elementOrData, editorConfig) => {
return EDITOR_TYPE.create(elementOrData, editorConfig)
.then(editor => {
if(this.element.disabled) {
editor.enableReadOnlyMode("readonly");
}
console.log(editor);
})
.catch(error => {
console.error(error);
});
});
watchdog.create(this.element, config).catch(error => {
console.error(error);
});
}
}

View file

@ -0,0 +1,114 @@
import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox";
import "../../css/bootbox_extensions.css";
export default class extends Controller {
static values = {
deleteMessage: String,
prototype: String,
}
static targets = ["target"];
/**
* Decodes escaped HTML entities
* @param {string} input
* @returns {string}
*/
htmlDecode(input) {
const doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
/**
* Generates a unique ID to be used for the new element
* @returns {string}
*/
generateUID() {
const long = (performance.now().toString(36)+Math.random().toString(36)).replace(/\./g,"");
return long.slice(0, 6); // 6 characters is enough for our unique IDs here
}
/**
* Create a new entry in the target using the given prototype value
*/
createElement(event) {
const targetTable = this.targetTarget;
const prototype = this.prototypeValue
if(!prototype) {
console.warn("Prototype is not set, we cannot create a new element. This is most likely due to missing permissions.");
bootbox.alert("You do not have the permsissions to create a new element. (No protoype element is set)");
return;
}
//Apply the index to prototype to create our element to insert
const newElementStr = this.htmlDecode(prototype.replace(/__name__/g, this.generateUID()));
//Insert new html after the last child element
//If the table has a tbody, insert it there
if(targetTable.tBodies[0]) {
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
} else { //Otherwise just insert it
targetTable.insertAdjacentHTML('beforeend', newElementStr);
}
}
/**
* Similar to createEvent Pricedetails need some special handling to fill min amount
* @param event
*/
createPricedetail(event) {
//First insert our new element
this.createElement(event);
const extractElementsFromRow = (row) => {
const priceRelated = row.querySelector("input[id$='price_related_quantity_value']");
const minDiscount = row.querySelector("input[id$='min_discount_quantity_value']");
return [priceRelated, minDiscount];
}
const targetTable = this.targetTarget;
const targetRows = targetTable.tBodies[0].rows;
const targetRowsCount = targetRows.length;
//If we just have one element we dont have to do anything as 1 is already the default
if(targetRowsCount <= 1) {
return;
}
//Our new element is the last child of the table
const newlyCreatedRow = targetRows[targetRowsCount - 1];
const [newPriceRelated, newMinDiscount] = extractElementsFromRow(newlyCreatedRow);
const oldRow = targetRows[targetRowsCount - 2];
const [oldPriceRelated, oldMinDiscount] = extractElementsFromRow(oldRow);
//Use the old PriceRelated value to determine the next 10 decade value for the new row
const oldMinAmount = parseInt(oldMinDiscount.value)
//The next 10 power can be achieved by creating a string beginning with "1" and adding 0 times the length of the old string
const oldMinAmountLength = oldMinAmount.toString().length;
const newMinAmountStr = '1' + '0'.repeat(oldMinAmountLength);
//Parse the sting back to an integer and we have our new min amount
const newMinAmount = parseInt(newMinAmountStr);
//Assign it to our new element
newMinDiscount.value = newMinAmount;
}
deleteElement(event) {
bootbox.confirm(this.deleteMessageValue, (result) => {
if(result) {
const target = event.target;
//Remove the row element from the table
target.closest("tr").remove();
}
});
}
}

View file

@ -0,0 +1,145 @@
import {Controller} from "@hotwired/stimulus";
//Styles
import 'datatables.net-bs5/css/dataTables.bootstrap5.css'
import 'datatables.net-buttons-bs5/css/buttons.bootstrap5.css'
import 'datatables.net-fixedheader-bs5/css/fixedHeader.bootstrap5.css'
import 'datatables.net-select-bs5/css/select.bootstrap5.css'
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.css';
//JS
import 'datatables.net-bs5';
import 'datatables.net-buttons-bs5';
import 'datatables.net-buttons/js/buttons.colVis.js';
import 'datatables.net-fixedheader-bs5';
import 'datatables.net-select-bs5';
import 'datatables.net-colreorder-bs5';
import 'datatables.net-responsive-bs5';
import '../../../js/lib/datatables';
const EVENT_DT_LOADED = 'dt:loaded';
export default class extends Controller {
static targets = ['dt'];
/** The datatable instance associated with this controller instance */
_dt;
connect() {
//$($.fn.DataTable.tables()).DataTable().fixedHeader.disable();
//$($.fn.DataTable.tables()).DataTable().destroy();
const settings = JSON.parse(this.element.dataset.dtSettings);
if(!settings) {
throw new Error("No settings provided for datatable!");
}
//Add url info, as the one available in the history is not enough, as Turbo may have not changed it yet
settings.url = this.element.dataset.dtUrl;
//@ts-ignore
const promise = $(this.dtTarget).initDataTables(settings,
{
colReorder: true,
responsive: true,
fixedHeader: {
header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
headerOffset: $("#navbar").height()
},
buttons: [{
"extend": 'colvis',
'className': 'mr-2 btn-light',
"text": "<i class='fa fa-cog'></i>"
}],
select: this.isSelectable(),
rowCallback: this._rowCallback.bind(this),
})
//Register error handler
.catch(err => {
console.error("Error initializing datatables: " + err);
});
//Dispatch an event to let others know that the datatables has been loaded
promise.then((dt) => {
const event = new CustomEvent(EVENT_DT_LOADED, {bubbles: true});
this.element.dispatchEvent(event);
this._dt = dt;
});
//Register event handlers
promise.then((dt) => {
dt.on('select.dt deselect.dt', this._onSelectionChange.bind(this));
});
//Allow to further configure the datatable
promise.then(this._afterLoaded.bind(this));
//Register links.
/*promise.then(function() {
//Set the correct title in the table.
let title = $('#part-card-header-src');
$('#part-card-header').html(title.html());
$(document).trigger('ajaxUI:dt_loaded');
if($table.data('part_table')) {
//@ts-ignore
$('#dt').on( 'select.dt deselect.dt', function ( e, dt, items ) {
let selected_elements = dt.rows({selected: true});
let count = selected_elements.count();
if(count > 0) {
$('#select_panel').removeClass('d-none');
} else {
$('#select_panel').addClass('d-none');
}
$('#select_count').text(count);
let selected_ids_string = selected_elements.data().map(function(value, index) {
return value['id']; }
).join(",");
$('#select_ids').val(selected_ids_string);
} );
}
//Attach event listener to update links after new page selection:
$('#dt').on('draw.dt column-visibility.dt', function() {
//ajaxUI.registerLinks();
$(document).trigger('ajaxUI:dt_loaded');
});
});*/
console.debug('Datatables inited.');
}
_rowCallback(row, data, index) {
//Empty by default but can be overridden by child classes
}
_onSelectionChange(e, dt, items ) {
//Empty by default but can be overridden by child classes
alert("Test");
}
_afterLoaded(dt) {
//Empty by default but can be overridden by child classes
}
/**
* Check if this datatable has selection feature enabled
*/
isSelectable()
{
return this.element.dataset.select ?? false;
}
}

View file

@ -0,0 +1,32 @@
import DatatablesController from "./datatables_controller.js";
/**
* This is the datatables controller for log pages, it includes an mechanism to color lines based on their level.
*/
export default class extends DatatablesController {
_rowCallback(row, data, index) {
//Check if we have a level, then change color of this row
if (data.level) {
let style = "";
switch (data.level) {
case "emergency":
case "alert":
case "critical":
case "error":
style = "table-danger";
break;
case "warning":
style = "table-warning";
break;
case "notice":
style = "table-info";
break;
}
if (style) {
$(row).addClass(style);
}
}
}
}

View file

@ -0,0 +1,128 @@
import DatatablesController from "./datatables_controller.js";
import * as bootbox from "bootbox";
/**
* This is the datatables controller for parts lists
*/
export default class extends DatatablesController {
static targets = ['dt', 'selectPanel', 'selectIDs', 'selectCount', 'selectTargetPicker'];
_confirmed = false;
isSelectable() {
//Parts controller is always selectable
return true;
}
_onSelectionChange(e, dt, items) {
const selected_elements = dt.rows({selected: true});
const count = selected_elements.count();
const selectPanel = this.selectPanelTarget;
//Hide/Unhide panel with the selection tools
if (count > 0) {
selectPanel.classList.remove('d-none');
} else {
selectPanel.classList.add('d-none');
}
//Update selection count text
this.selectCountTarget.innerText = count;
//Fill selection ID input
let selected_ids_string = selected_elements.data().map(function(value, index) {
return value['id']; }
).join(",");
this.selectIDsTarget.value = selected_ids_string;
}
updateOptions(select_element, json)
{
//Clear options
select_element.innerHTML = null;
$(select_element).selectpicker('destroy');
for(let i=0; i<json.length; i++) {
let json_opt = json[i];
let opt = document.createElement('option');
opt.value = json_opt.value;
opt.innerHTML = json_opt.text;
if(json_opt['data-subtext']) {
opt.dataset.subtext = json_opt['data-subtext'];
}
select_element.appendChild(opt);
}
$(select_element).selectpicker('show');
}
updateTargetPicker(event) {
const element = event.target;
//Extract the url from the selected option
const selected_option = element.options[element.options.selectedIndex];
const url = selected_option.dataset.url;
const select_target = this.selectTargetPickerTarget;
if (url) {
fetch(url)
.then(response => {
response.json().then(json => {
this.updateOptions(select_target, json);
});
});
} else {
$(select_target).selectpicker('hide');
}
}
confirmDeletionAtSubmit(event) {
//Only show the dialog when selected action is delete
if (event.target.elements["action"].value !== "delete") {
return;
}
//If a user has not already confirmed the deletion, just let turbo do its work
if(this._confirmed) {
this._confirmed = false;
return;
}
//Prevent turbo from doing its work
event.preventDefault();
const message = this.element.dataset.deleteMessage;
const title = this.element.dataset.deleteTitle;
const form = this.element;
const that = this;
//Create a clone of the event with the same submitter, so we can redispatch it if needed
//We need to do this that way, as we need the submitter info, just calling form.submit() would not work
this._our_event = new SubmitEvent('submit', {
submitter: event.submitter,
bubbles: true, //This line is important, otherwise Turbo will not receive the event
});
const confirm = bootbox.confirm({
message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form.
if (result) {
that._confirmed = true;
form.dispatchEvent(that._our_event);
} else {
that._confirmed = false;
}
}
});
}
}

View file

@ -0,0 +1,48 @@
import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox";
import "../../css/bootbox_extensions.css";
export default class extends Controller
{
connect()
{
this._confirmed = false;
}
submit(event) {
//If a user has not already confirmed the deletion, just let turbo do its work
if(this._confirmed) {
this._confirmed = false;
return;
}
//Prevent turbo from doing its work
event.preventDefault();
const message = this.element.dataset.deleteMessage;
const title = this.element.dataset.deleteTitle;
const form = this.element;
const that = this;
//Create a clone of the event with the same submitter, so we can redispatch it if needed
//We need to do this that way, as we need the submitter info, just calling form.submit() would not work
this._our_event = new SubmitEvent('submit', {
submitter: event.submitter,
bubbles: true, //This line is important, otherwise Turbo will not receive the event
});
const confirm = bootbox.confirm({
message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form.
if (result) {
that._confirmed = true;
form.dispatchEvent(that._our_event);
} else {
that._confirmed = false;
}
}
});
}
}

View file

@ -0,0 +1,21 @@
import {Controller} from "@hotwired/stimulus";
import {Popover} from "bootstrap";
export default class extends Controller {
connect() {
const thumbnail_url = this.element.dataset.thumbnail;
this._popover = Popover.getOrCreateInstance(this.element, {
html: true,
trigger: 'hover',
placement: 'right',
container: 'body',
'title': this.element.dataset.title ?? '',
content: function () {
return '<img class="img-fluid" src="' + thumbnail_url + '" />';
}
});
this._popover.hide();
}
}

View file

@ -0,0 +1,16 @@
import {Controller} from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["input"];
connect() {
if(!this.inputTarget) {
throw new Error("Target input not found");
}
}
load(event) {
//Use the data-value attribute to load the value of our target input
this.inputTarget.value = event.target.dataset.value;
}
}

View file

@ -0,0 +1,15 @@
const bootstrap = window.bootstrap = require('bootstrap'); // without this bootstrap-select crashes with `undefined bootstrap`
require('bootstrap-select/js/bootstrap-select'); // we have to manually require the working js file
import {Controller} from "@hotwired/stimulus";
import "bootstrap-select/dist/css/bootstrap-select.css";
import "../../css/selectpicker_extensions.css";
export default class extends Controller {
connect() {
$(this.element).selectpicker({
dropdownAlignRight: 'auto',
container: '#content',
});
}
}

View file

@ -0,0 +1,68 @@
import {Controller} from "@hotwired/stimulus";
import {default as TreeController} from "./tree_controller";
import "patternfly-bootstrap-treeview/src/css/bootstrap-treeview.css"
import "patternfly-bootstrap-treeview";
export default class extends TreeController {
static targets = [ "tree", 'sourceText' ];
_storage_key;
connect() {
//Check if the tree is already initialized, if so then skip initialization (useful when going back) to in history using Turbo
if(this._isInitialized()) {
return;
}
const default_mode = this.element.dataset.defaultMode;
this._storage_key = 'tree_' + this.element.id;
//Check if we have a saved mode
const stored_mode = localStorage.getItem(this._storage_key);
//Use stored mode if possible, otherwise use default
if(stored_mode) {
try {
this.setMode(stored_mode);
} catch (e) {
console.error(e);
//If an error happenes, use the default mode
this.setMode(default_mode);
}
} else {
this.setMode(default_mode);
}
}
setMode(mode) {
//Find the button for this mode
const modeButton = this.element.querySelector(`[data-mode="${mode}"]`);
if(!modeButton) {
throw new Error(`Could not find button for mode ${mode}`);
}
//Get the url and text from the button
const url = modeButton.dataset.url;
const text = modeButton.dataset.text;
this.sourceTextTarget.innerText = text;
this.setURL(url);
}
changeDataSource(event)
{
const mode = event.params.mode ?? event.target.dataset.mode;
const url = event.params.url ?? event.target.dataset.url;
const text = event.params.text ?? event.target.dataset.text;
this.sourceTextTarget.innerText = text;
this.setURL(url);
//Save the mode in local storage
localStorage.setItem(this._storage_key, mode);
}
}

View file

@ -0,0 +1,68 @@
import {Controller} from "@hotwired/stimulus";
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/tom-select_extensions.css';
import TomSelect from "tom-select";
export default class extends Controller {
_tomSelect;
connect() {
let settings = {
plugins: {
remove_button:{
}
},
persistent: false,
createOnBlur: true,
create: true,
};
if(this.element.dataset.autocomplete) {
const base_url = this.element.dataset.autocomplete;
settings.load = (query, callback) => {
if(query.length < 2){
callback();
return;
}
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
fetch(url)
.then(response => response.json())
.then(json => {
const data = json.map(x => {return {"value": x, "text": x}});
callback(data);
}).catch(()=>{
callback();
});
}
}
this._tomSelect = new TomSelect(this.element, settings);
/*if(this.element.dataset.autocomplete) {
const engine = new Bloodhound({
//@ts-ignore
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
//@ts-ignore
queryTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
remote: {
url: this.element.dataset.autocomplete,
wildcard: 'QUERY'
}
});
$(this.element).tagsinput({
typeaheadjs: {
name: 'tags',
source: engine.ttAdapter()
}
});
} else { // Init tagsinput without typeahead
$(this.element).tagsinput();
}*/
}
}

View file

@ -0,0 +1,130 @@
import {Controller} from "@hotwired/stimulus";
import "patternfly-bootstrap-treeview/src/css/bootstrap-treeview.css"
import "patternfly-bootstrap-treeview";
export default class extends Controller {
static targets = [ "tree" ];
_url = null;
_data = null;
connect() {
const treeElement = this.treeTarget;
if (!treeElement) {
console.error("You need to define a tree target for the controller!");
return;
}
this._url = this.element.dataset.treeUrl;
this._data = this.element.dataset.treeData;
this.reinitTree();
}
reinitTree()
{
//Fetch data and initialize tree
this._getData()
.then(this._fillTree.bind(this))
.catch((err) => {
console.error("Could not load the tree data: " + err);
});
}
setData(data) {
this._data = data;
this.reinitTree();
}
setURL(url) {
this._url = url;
this.reinitTree();
}
_fillTree(data) {
//Get primary color from css variable
const primary_color = getComputedStyle(document.documentElement).getPropertyValue('--bs-warning');
const tree = this.treeTarget;
$(tree).treeview({
data: data,
enableLinks: true,
showIcon: false,
showBorder: true,
searchResultBackColor: primary_color,
searchResultColor: '#000',
onNodeSelected: function (event, data) {
if (data.href) {
//Simulate a click so we just change the inner frame
let a = document.createElement('a');
a.setAttribute('href', data.href);
a.innerHTML = "";
$(tree).append(a);
a.click();
a.remove();
}
},
//onNodeContextmenu: contextmenu_handler,
expandIcon: "fas fa-plus fa-fw fa-treeview",
collapseIcon: "fas fa-minus fa-fw fa-treeview"
})
.on('initialized', function () {
//Collapse all nodes after init
$(this).treeview('collapseAll', {silent: true});
//Reveal the selected ones
$(this).treeview('revealNode', [$(this).treeview('getSelected')]);
});
}
collapseAll() {
$(this.treeTarget).treeview('collapseAll', {silent: true});
}
expandAll() {
$(this.treeTarget).treeview('expandAll', {silent: true});
}
searchInput(event) {
const data = event.target.value;
//Do nothing if no data was passed
const tree = this.treeTarget;
$(tree).treeview('collapseAll', {silent: true});
$(tree).treeview('search', [data]);
}
/**
* Check if the tree is already initialized (meaning bootstrap treeview was called on the object)
* @private
*/
_isInitialized() {
const $tree = $(this.treeTarget).treeview(true);
//If the tree is not initialized yet, we just get an empty jquery object with the treeview functions missing
if(typeof $tree.findNodes === 'undefined' ) {
return false;
}
return true;
}
_getData() {
//Use lambda function to preserve this context
return new Promise((myResolve, myReject) => {
//If a url is defined, fetch the data from the url
if (this._url) {
return fetch(this._url)
.then((response) => myResolve(response.json()))
.catch((err) => myReject(err));
}
//Otherwise load the data provided via the data attribute
return myResolve(this._data);
});
}
}

View file

@ -1,16 +0,0 @@
import { Controller } from 'stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

View file

@ -0,0 +1,86 @@
import {Controller} from "@hotwired/stimulus";
import * as ZXing from "@zxing/library";
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [ "source" ]
codeReader = null;
connect() {
console.log('Init Scanner');
this.codeReader = new ZXing.BrowserMultiFormatReader();
this.initScanner();
}
codeScannedHandler(result, err) {
if (result) {
//@ts-ignore
document.getElementById('scan_dialog_input').value = result.text;
//Submit form
//@ts-ignore
document.getElementById('scan_dialog_form').submit();
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error(err);
//document.getElementById('result').textContent = err
}
}
initScanner() {
let selectedDeviceId;
this.codeReader.listVideoInputDevices()
.then((videoInputDevices) => {
if (videoInputDevices.length >= 1) {
const sourceSelect = document.getElementById('sourceSelect');
videoInputDevices.forEach((element) => {
const sourceOption = document.createElement('option');
sourceOption.text = element.label;
sourceOption.value = element.deviceId;
sourceSelect.appendChild(sourceOption);
});
//Try to retrieve last selected webcam...
let last_cam_id = localStorage.getItem('scanner_last_cam_id');
if (!!last_cam_id) {
//selectedDeviceId = localStorage.getItem('scanner_last_cam_id');
sourceSelect.value = last_cam_id;
} else {
selectedDeviceId = videoInputDevices[0].deviceId;
}
sourceSelect.onchange = () => {
//@ts-ignore
selectedDeviceId = sourceSelect.value;
localStorage.setItem('scanner_last_cam_id', selectedDeviceId);
changeHandler();
};
document.getElementById('sourceSelectPanel').classList.remove('d-none');
document.getElementById('video').classList.remove('d-none');
document.getElementById('scanner-warning').classList.add('d-none');
}
let changeHandler = () => {
this.codeReader.reset();
this.codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => this.codeScannedHandler(result, err));
console.log(`Started continous decode from camera with id ${selectedDeviceId}`)
};
//Register Change Src Button
//document.getElementById('changeSrcBtn').addEventListener('click', changeHandler);
//Try to start logging automatically.
changeHandler();
})
.catch((err) => {
console.error(err)
})
}
}

View file

@ -0,0 +1,8 @@
import {Controller} from "@hotwired/stimulus";
export default class extends Controller
{
download(event) {
this.element.href = document.getElementById('pdf_preview').data
}
}

View file

@ -0,0 +1,53 @@
import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox";
export default class extends Controller {
static values = {
errorMissingValues: String,
errorOuterGreaterInner: String,
}
updateReelCalc() {
const dia_inner = document.getElementById('reel_dia_inner').value;
const dia_outer = document.getElementById('reel_dia_outer').value;
const tape_thickness = document.getElementById('reel_tape_thick').value;
const part_distance = document.getElementById('reel_part_distance').value;
if (dia_inner == "" || dia_outer == "" || tape_thickness == "") {
bootbox.alert(this.errorMissingValuesValue);
return;
}
if (dia_outer**dia_outer < dia_inner**dia_inner) {
bootbox.alert(this.errorOuterGreaterInnerValue);
return;
}
const length = Math.PI * (dia_outer * dia_outer - dia_inner * dia_inner ) / (4 * tape_thickness);
let length_formatted = length.toFixed(2) + ' mm';
if (length > 1000) {
length_formatted = (length / 1000).toFixed(2) + ' m';
} else if (length > 10) {
length_formatted = (length / 10).toFixed(2) + ' cm';
}
document.getElementById('result_length').textContent = length_formatted;
//Skip if no part_distance was given
if (part_distance == "" || part_distance == 0) {
return;
}
var parts_per_meter = 1 / (part_distance / 1000);
document.getElementById('result_parts_per_meter').textContent = parts_per_meter.toFixed(2) + ' 1/m';
var parts_amount = (length/1000) * parts_per_meter;
document.getElementById('result_amount').textContent = Math.floor(parts_amount);
}
}

View file

@ -0,0 +1,10 @@
import {Controller} from "@hotwired/stimulus";
export default class extends Controller
{
connect() {
this.element.onclick = function() {
window.u2fauth.register();
}
}
}

View file

@ -0,0 +1,8 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
//If we encounter an element with global reload controller, then reload the whole page
window.location.reload();
}
}

View file

@ -0,0 +1,8 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
const menu = document.getElementById('locale-select-menu');
menu.innerHTML = this.element.innerHTML;
}
}

View file

@ -0,0 +1,12 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
//If we encounter an element with this, then change the title of our document according to data-title
this.changeTitle(this.element.dataset.title);
}
changeTitle(title) {
document.title = title;
}
}

View file

@ -25,11 +25,11 @@
/* Add padding for fixed header bar */
body {
overflow-x: hidden;
position: relative;
/*position: relative;*/
padding-top: 70px;
/* Use font size like in BS3 */
font-size: 14px;
line-height: 1.428;
/*font-size: 14px;
line-height: 1.428;*/
}
@media (min-width: 768px) {
@ -69,8 +69,6 @@ body {
/*noinspection CssUnknownProperty*/
scrollbar-width: none;
}
}
/*noinspection W3CssValidation*/
@ -242,6 +240,9 @@ showing the sidebar (on devices with md or higher)
/****************************************
* Bootstrap extensions
*****************************************/
a.badge {
text-decoration: none;
}
.w-fit {
width: -moz-fit-content;
@ -420,6 +421,13 @@ table .input-group-btn:last-child>.btn, table .input-group-btn:last-child>.btn-g
display: none;
}
}
/**
* Enforce white links on selected rows in datatables
*/
table.dataTable > tbody > tr.selected > td > a {
color: white !important;
}
/***************************************
* Dropdown with radio buttons
***************************************/
@ -792,43 +800,6 @@ div.dataTables_wrapper div.dataTables_info {
margin: 0;
}
/***************************************************
* Markdown styles
***************************************************/
.markdown code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
.markdown pre {
display: block;
padding: 10px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
.markdown img {
max-width: 35%;
vertical-align: middle;
}
.markdown blockquote {
padding: 10px 10px;
margin: 0 0 10px;
font-size: 18px;
border-left: 5px solid #aaa;
}
.darkmode-layer {
z-index: 2020;
}
@ -884,4 +855,5 @@ div.dataTables_wrapper div.dataTables_info {
***********************************************/
.typeahead-image {
width: 100%;
}
}

View file

@ -0,0 +1,27 @@
.modal-body > .bootbox-close-button {
position: absolute;
top: 0;
right: 0;
padding: 0.5rem 0.75rem;
z-index: 1;
}
.modal .bootbox-close-button {
font-weight: 100;
}
button.bootbox-close-button {
padding: 0;
background-color: transparent;
border: 0;
-webkit-appearance: none;
}
.bootbox-close-button {
/* float: right; */
font-size: 1.40625rem;
font-weight: 600;
line-height: 1;
color: #000;
text-shadow: none;
opacity: .5;
}

53
assets/css/markdown.css Normal file
View file

@ -0,0 +1,53 @@
/**
* Customization for markdown output
*/
.markdown .text-tiny {
font-size: .7em;
}
.markdown .text-small {
font-size: .85em;
}
.markdown .text-big {
font-size: 1.4em;
}
.markdown .text-huge {
font-size: 1.8em;
}
.markdown code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
.markdown pre {
display: block;
padding: 10px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333;
word-break: break-all;
word-wrap: break-word;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
}
.markdown img {
max-width: 35%;
vertical-align: middle;
}
.markdown blockquote {
padding: 10px 10px;
margin: 0 0 10px;
font-size: 18px;
border-left: 5px solid #aaa;
}

View file

@ -0,0 +1,14 @@
/***********************************************
* Special level whitespace characters that only show up when inside a bootstrap-select dropdown
***********************************************/
.dropdown-item span.picker-level::after {
content: "\00a0\00a0\00a0"; /* 3 spaces */
}
/** Bootstrap-select Hide on Selected element */
.picker-hs {
display: none;
}
.dropdown-item .picker-hs {
display: inherit;
}

View file

@ -1,64 +0,0 @@
/*
* bootstrap-tagsinput v0.8.0
* Modified by Jan Böhmer 2019
* (Added some margin between the tags.
*/
.bootstrap-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
color: #555;
vertical-align: middle;
border-radius: 4px;
width: 100%;
line-height: 22px;
cursor: text;
}
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0 6px;
margin: 0;
width: auto;
max-width: inherit;
}
.bootstrap-tagsinput.form-control input::-moz-placeholder {
color: #777;
opacity: 1;
}
.bootstrap-tagsinput.form-control input:-ms-input-placeholder {
color: #777;
}
.bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
color: #777;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .badge {
margin: 2px 0.2em;
padding:5px 8px;
}
.bootstrap-tagsinput .badge [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.bootstrap-tagsinput .badge [data-role="remove"]:after {
content: "×";
padding: 0px 4px;
background-color:rgba(0, 0, 0, 0.1);
border-radius:50%;
font-size:13px
}
.bootstrap-tagsinput .badge [data-role="remove"]:hover:after {
background-color:rgba(0, 0, 0, 0.62);}
.bootstrap-tagsinput .badge [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

View file

@ -0,0 +1,4 @@
.tagsinput.ts-wrapper.multi .ts-control > div {
background: var(--bs-secondary);
color: var(--bs-white);
}

View file

@ -17,116 +17,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// Main CSS files
import '../css/app.css';
// any CSS you require will output into a single css file (app.css in this case)
require('../css/app.css');
// start the Stimulus application
import '../bootstrap';
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
const $ = require('jquery');
//Only include javascript
import '@fortawesome/fontawesome-free/css/all.css'
import 'datatables.net-bs4/css/dataTables.bootstrap4.css'
import 'datatables.net-buttons-bs4/css/buttons.bootstrap4.css'
import 'datatables.net-fixedheader-bs4/css/fixedHeader.bootstrap4.css'
import 'datatables.net-select-bs4/css/select.bootstrap4.css'
import 'bootstrap-select/dist/css/bootstrap-select.css'
import "patternfly-bootstrap-treeview/src/css/bootstrap-treeview.css"
import "bootstrap-fileinput/css/fileinput.css"
require('bootstrap');
//require( 'jszip' );
//#require( 'pdfmake' );
require( 'datatables.net-bs4' );
require( 'datatables.net-buttons-bs4' );
require( 'datatables.net-buttons/js/buttons.colVis.js' );
//require( 'datatables.net-buttons/js/buttons.html5.js' );
//require( 'datatables.net-buttons/js/buttons.print.js' );
require( 'datatables.net-fixedheader-bs4' );
require( 'datatables.net-select-bs4' );
require('datatables.net-colreorder-bs4');
require('datatables.net-responsive-bs4');
require('datatables.net-responsive-bs4/css/responsive.bootstrap4.css');
require('bootstrap-select');
require('jquery-form');
require('corejs-typeahead/dist/typeahead.bundle.min');
window.Bloodhound = require('corejs-typeahead/dist/bloodhound.js');
import "./error_handler";
import "./tab_remember";
import "./register_events";
import "./tristate_checkboxes";
//Define jquery globally
window.$ = window.jQuery = require("jquery");
require('patternfly-bootstrap-treeview/src/js/bootstrap-treeview');
require('bootstrap-fileinput');
require('./datatables.js');
window.bootbox = require('bootbox');
require("marked");
window.DOMPurify = require("dompurify");
// Includes required for tag input
require('./tagsinput.js');
require('../css/tagsinput.css');
//Tristate checkbox support
require('./jquery.tristate.js');
require('darkmode-js');
//Equation rendering
require('katex');
window.renderMathInElement = require('katex/contrib/auto-render/auto-render').default;
import 'katex/dist/katex.css';
window.ClipboardJS = require('clipboard');
require('../ts_src/ajax_ui');
import {ajaxUI} from "../ts_src/ajax_ui";
window.ajaxUI = ajaxUI;
//Require all events;
require('../ts_src/event_listeners');
//Register darkmode (we must do it here, TS does not support ES6 constructor...
try {
//The browser needs to support mix blend mode
if(typeof window.getComputedStyle(document.body).mixBlendMode !== 'undefined') {
const darkmode = new Darkmode();
$(document).on("ajaxUI:start ajaxUI:reload", function () {
//Show darkmode toggle to user
$('#toggleDarkmodeContainer, #toggleDarkmodeSeparator').removeAttr('hidden');
$('#toggleDarkmode').prop('checked', darkmode.isActivated());
$('#toggleDarkmode').change(function () {
darkmode.toggle();
});
});
}
} catch (e) {
//Ignore all errors (for compatibiltiy with old browsers)
}
//Start AjaxUI AFTER all event has been registered
$(document).ready(ajaxUI.start());
//console.log('Hello Webpack Encore! Edit me in assets/js/app.js');
window.$ = window.jQuery = require("jquery")

View file

@ -0,0 +1,81 @@
import * as bootbox from "bootbox";
/**
* If this class is imported the user is shown an error dialog if he calls an page via Turbo and an error is responded.
* @type {ErrorHandlerHelper}
*/
class ErrorHandlerHelper {
constructor() {
console.log('Error Handler registered');
const content = document.getElementById('content');
content.addEventListener('turbo:before-fetch-response', (event) => this.handleError(event));
}
handleError(event) {
const fetchResponse = event.detail.fetchResponse;
const response = fetchResponse.response;
//Ignore aborted requests.
if (response.statusText === 'abort' || response.status == 0) {
return;
}
//Ignore status 422 as this means a symfony validation error occured and we need to show it to user. This is no (unexpected) error.
if (response.status == 422) {
return;
}
if(fetchResponse.failed) {
//Create error text
let title = response.statusText + ' (Status ' + response.status + ')';
/**
switch(response.status) {
case 500:
title = 'Internal Server Error!';
break;
case 404:
title = "Site not found!";
break;
case 403:
title = "Permission denied!";
break;
} **/
const alert = bootbox.alert(
{
size: 'large',
message: function() {
let url = fetchResponse.location.toString();
let msg = `Error calling <a href="${url}">${url}</a>. `;
msg += 'Try to reload the page or contact the administrator if this error persists.'
msg += '<br><br><a class=\"btn btn-link\" data-bs-toggle=\"collapse\" href=\"#iframe_div\" >' + 'View details' + "</a>";
msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='error-iframe'></iframe></div>";
return msg;
},
title: title,
callback: function () {
//Remove blur
$('#content').removeClass('loading-content');
}
});
//@ts-ignore
alert.init(function (){
response.text().then( (html) => {
var dstFrame = document.getElementById('error-iframe');
//@ts-ignore
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
dstDoc.write(html)
dstDoc.close();
});
});
}
}
}
export default new ErrorHandlerHelper();

View file

@ -43,7 +43,7 @@
return new Promise((fulfill, reject) => {
// Perform initial load
$.ajax(config.url, {
$.ajax(typeof config.url === 'function' ? config.url(null) : config.url, {
method: config.method,
data: {
_dt: config.name,
@ -53,15 +53,17 @@
var baseState;
// Merge all options from different sources together and add the Ajax loader
var dtOpts = $.extend({}, data.options, config.options, options, persistOptions, {
var dtOpts = $.extend({}, data.options, typeof config.options === 'function' ? {} : config.options, options, persistOptions, {
ajax: function (request, drawCallback, settings) {
if (data) {
data.draw = request.draw;
drawCallback(data);
data = null;
if (Object.keys(state).length && dt.state != null) {
var merged = $.extend(true, {}, dt.state(), state);
dt
if (Object.keys(state).length) {
var api = new $.fn.dataTable.Api( settings );
var merged = $.extend(true, {}, api.state(), state);
api
.order(merged.order)
.search(merged.search.search)
.page.len(merged.length)
@ -70,7 +72,7 @@
}
} else {
request._dt = config.name;
$.ajax(config.url, {
$.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, {
method: config.method,
data: request
}).done(function(data) {
@ -80,6 +82,10 @@
}
});
if (typeof config.options === 'function') {
dtOpts = config.options(dtOpts);
}
root.html(data.template);
dt = $('table', root).DataTable(dtOpts);
if (config.state !== 'none') {
@ -122,6 +128,80 @@
url: window.location.origin + window.location.pathname
};
/**
* Server-side export.
*/
$.fn.initDataTables.exportBtnAction = function(exporterName, settings) {
settings = $.extend({}, $.fn.initDataTables.defaults, settings);
return function(e, dt) {
const params = $.param($.extend({}, dt.ajax.params(), {'_dt': settings.name, '_exporter': exporterName}));
// Credit: https://stackoverflow.com/a/23797348
const xhr = new XMLHttpRequest();
xhr.open(settings.method, settings.method === 'GET' ? (settings.url + '?' + params) : settings.url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (this.status === 200) {
let filename = "";
const disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
const type = xhr.getResponseHeader('Content-Type');
let blob;
if (typeof File === 'function') {
try {
blob = new File([this.response], filename, { type: type });
} catch (e) { /* Edge */ }
}
if (typeof blob === 'undefined') {
blob = new Blob([this.response], { type: type });
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
}
else {
const URL = window.URL || window.webkitURL;
const downloadUrl = URL.createObjectURL(blob);
if (filename) {
// use HTML5 a[download] attribute to specify filename
const a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
}
else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
}
else {
window.location = downloadUrl;
}
setTimeout(function() { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
}
}
};
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(settings.method === 'POST' ? params : null);
}
};
/**
* Convert a querystring to a proper array - reverses $.param
*/
@ -182,4 +262,4 @@
return obj;
}
}($));
}(jQuery));

View file

@ -0,0 +1,104 @@
'use strict';
class RegisterEventHelper {
constructor() {
this.registerTooltips();
this.registerSpecialCharInput();
}
registerLoadHandler(fn) {
document.addEventListener('turbo:render', fn);
document.addEventListener('turbo:load', fn);
}
registerTooltips() {
this.registerLoadHandler(() => {
$(".tooltip").remove();
//Exclude dropdown buttons from tooltips, otherwise we run into endless errors from bootstrap (bootstrap.esm.js:614 Bootstrap doesn't allow more than one instance per element. Bound instance: bs.dropdown.)
$('a[title], button[title]:not([data-bs-toggle="dropdown"]), span[title], h6[title], h3[title], i.fas[title]')
//@ts-ignore
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
});
}
registerSpecialCharInput() {
this.registerLoadHandler(() => {
//@ts-ignore
$("input[type=text], input[type=search]").unbind("keydown").keydown(function (event) {
let greek = event.altKey;
let greek_char = "";
if (greek){
switch(event.key) {
case "w": //Omega
greek_char = '\u2126';
break;
case "u":
case "m": //Micro
greek_char = "\u00B5";
break;
case "p": //Phi
greek_char = "\u03C6";
break;
case "a": //Alpha
greek_char = "\u03B1";
break;
case "b": //Beta
greek_char = "\u03B2";
break;
case "c": //Gamma
greek_char = "\u03B3";
break;
case "d": //Delta
greek_char = "\u03B4";
break;
case "l": //Pound
greek_char = "\u00A3";
break;
case "y": //Yen
greek_char = "\u00A5";
break;
case "o": //Yen
greek_char = "\u00A4";
break;
case "1": //Sum symbol
greek_char = "\u2211";
break;
case "2": //Integral
greek_char = "\u222B";
break;
case "3": //Less-than or equal
greek_char = "\u2264";
break;
case "4": //Greater than or equal
greek_char = "\u2265";
break;
case "5": //PI
greek_char = "\u03c0";
break;
case "q": //Copyright
greek_char = "\u00A9";
break;
case "e": //Euro
greek_char = "\u20AC";
break;
}
if(greek_char=="") return;
let $txt = $(this);
//@ts-ignore
let caretPos = $txt[0].selectionStart;
let textAreaTxt = $txt.val().toString();
$txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) );
}
});
//@ts-ignore
this.greek_once = true;
})
}
}
export default new RegisterEventHelper();

101
assets/js/tab_remember.js Normal file
View file

@ -0,0 +1,101 @@
"use strict";
import {Tab} from "bootstrap";
import tab from "bootstrap/js/src/tab";
/**
* This listener keeps track of which tab is currently selected (using hash and localstorage) and will try to open
* that tab on reload. That means that if the user changes something, he does not have to switch back to the tab
* where he was before submit.
*/
class TabRememberHelper {
constructor() {
document.addEventListener("turbo:load", this.onLoad.bind(this));
document.addEventListener("turbo:frame-render", this.handleSymfonyValidationErrors.bind(this));
//Capture is important here, as invalid events normally does not bubble
document.addEventListener("invalid", this.onInvalid.bind(this), {capture: true});
}
handleSymfonyValidationErrors(event) {
const responseCode = event.detail.fetchResponse.response.status;
//We only care about 422 (symfony validation error)
if(responseCode !== 422) {
return;
}
//Find the first offending element and show it
//Symfony validation errors can occur on multiple types
const inputErrors = document.getElementsByClassName('is-invalid');
const blockErrors = document.getElementsByClassName('form-error-message');
const merged = [...inputErrors, ...blockErrors];
const first_element = merged[0] ?? null;
if(first_element) {
this.revealElementOnTab(first_element);
}
debugger;
}
/**
* This functions is called when the browser side input validation fails on an input, jump to the tab to show this up
* @param event
*/
onInvalid(event) {
this.revealElementOnTab(event.target);
}
revealElementOnTab(element) {
let parent = element.closest('.tab-pane');
//Iterate over each parent tab and show it
while(parent) {
//Invoker can either be a button or a element
let tabInvoker = document.querySelector("button[data-content='#" + parent.id + "']")
?? document.querySelector("a[href='#" + parent.id + "']");
Tab.getOrCreateInstance(tabInvoker).show();
parent = parent.parentElement.closest('.tab-pane');
}
}
onLoad(event) {
//Determine which tab should be shown (use hash if specified, otherwise use localstorage)
let activeTab = null;
if (location.hash) {
activeTab = document.querySelector('[href=\'' + location.hash + '\']');
} else if (localStorage.getItem('activeTab')) {
activeTab = document.querySelector('[href="' + localStorage.getItem('activeTab') + '"]');
}
if (activeTab) {
//Reveal our tab selector (needed for nested tabs)
this.revealElementOnTab(activeTab);
//Finally show the active tab itself
Tab.getOrCreateInstance(activeTab).show();
}
//Register listener for tab change
document.addEventListener('shown.bs.tab', this.onTabChange.bind(this));
}
onTabChange(event) {
const tab = event.target;
let tab_name = tab.getAttribute('href')
if (history.replaceState) {
history.replaceState(null, null, tab_name)
} else {
location.hash = tab_name
}
localStorage.setItem('activeTab', tab_name)
}
}
export default new TabRememberHelper();

View file

@ -1,687 +0,0 @@
/*
* bootstrap-tagsinput v0.8.0
*
*/
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'badge badge-info';
},
focusClass: 'focus',
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
itemTitle: function(item) {
return null;
},
freeInput: true,
addOnBlur: true,
maxTags: undefined,
maxChars: undefined,
confirmKeys: [13, 44],
delimiter: ',',
delimiterRegex: null,
cancelConfirmKeysOnEmpty: false,
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
},
trimValue: false,
allowDuplicates: false,
triggerChange: true
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.isInit = true;
this.itemsArray = [];
this.$element = $(element);
this.$element.addClass('sr-only');
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.before(this.$container);
this.build(options);
this.isInit = false;
}
TagsInput.prototype = {
constructor: TagsInput,
/**
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
* updating the elements val()
*/
add: function(item, dontPushVal, options) {
var self = this;
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
return;
// Ignore falsey values, except false
if (item !== false && !item)
return;
// Trim value
if (typeof item === "string" && self.options.trimValue) {
item = $.trim(item);
}
// Throw an error when trying to add an object while the itemValue option was not set
if (typeof item === "object" && !self.objectItems)
throw("Can't add objects when itemValue option is not set");
// Ignore strings only containg whitespace
if (item.toString().match(/^\s*$/))
return;
// If SELECT but not multiple, remove current tag
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
self.remove(self.itemsArray[0]);
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
var items = item.split(delimiter);
if (items.length > 1) {
for (var i = 0; i < items.length; i++) {
this.add(items[i], true);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
return;
}
}
var itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item),
itemTitle = self.options.itemTitle(item);
// Ignore items allready added
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
if (existing && !self.options.allowDuplicates) {
// Invoke onTagExists
if (self.options.onTagExists) {
var $existingTag = $(".badge", self.$container).filter(function() { return $(this).data("item") === existing; });
self.options.onTagExists(item, $existingTag);
}
return;
}
// if length greater than limit
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
return;
// raise beforeItemAdd arg
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
self.$element.trigger(beforeItemAddEvent);
if (beforeItemAddEvent.cancel)
return;
// register item in internal array and map
self.itemsArray.push(item);
// add a tag element
var $tag = $('<span class="badge ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
$tag.data('item', item);
self.findInputWrapper().before($tag);
// Check to see if the tag exists in its raw or uri-encoded form
var optionExists = (
$('option[value="' + encodeURIComponent(itemValue) + '"]', self.$element).length ||
$('option[value="' + htmlEncode(itemValue) + '"]', self.$element).length
);
// add <option /> if item represents a value not present in one of the <select />'s options
if (self.isSelect && !optionExists) {
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
$option.data('item', item);
$option.attr('value', itemValue);
self.$element.append($option);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
// Add class when reached maxTags
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
self.$container.addClass('bootstrap-tagsinput-max');
// If using typeahead, once the tag has been added, clear the typeahead value so it does not stick around in the input.
if ($('.typeahead, .twitter-typeahead', self.$container).length) {
self.$input.typeahead('val', '');
}
if (this.isInit) {
self.$element.trigger($.Event('itemAddedOnInit', { item: item, options: options }));
} else {
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
}
},
/**
* Removes the given item. Pass true to dontPushVal to prevent updating the
* elements val()
*/
remove: function(item, dontPushVal, options) {
var self = this;
if (self.objectItems) {
if (typeof item === "object")
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
else
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
item = item[item.length-1];
}
if (item) {
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
self.$element.trigger(beforeItemRemoveEvent);
if (beforeItemRemoveEvent.cancel)
return;
$('.badge', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
if($.inArray(item, self.itemsArray) !== -1)
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
}
if (!dontPushVal)
self.pushVal(self.options.triggerChange);
// Remove class when reached maxTags
if (self.options.maxTags > self.itemsArray.length)
self.$container.removeClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
},
/**
* Removes all items
*/
removeAll: function() {
var self = this;
$('.badge', self.$container).remove();
$('option', self.$element).remove();
while(self.itemsArray.length > 0)
self.itemsArray.pop();
self.pushVal(self.options.triggerChange);
},
/**
* Refreshes the tags so they match the text/value of their corresponding
* item.
*/
refresh: function() {
var self = this;
$('.badge', self.$container).each(function() {
var $tag = $(this),
item = $tag.data('item'),
itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Update tag's class and inner text
$tag.attr('class', null);
$tag.addClass('badge ' + htmlEncode(tagClass));
$tag.contents().filter(function() {
return this.nodeType == 3;
})[0].nodeValue = htmlEncode(itemText);
if (self.isSelect) {
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
option.attr('value', itemValue);
}
});
},
/**
* Returns the items added as tags
*/
items: function() {
return this.itemsArray;
},
/**
* Assembly value by retrieving the value of each item, and set it on the
* element.
*/
pushVal: function() {
var self = this,
val = $.map(self.items(), function(item) {
return self.options.itemValue(item).toString();
});
self.$element.val( val.join(self.options.delimiter) );
if (self.options.triggerChange)
self.$element.trigger('change');
},
/**
* Initializes the tags input behaviour on the element
*/
build: function(options) {
var self = this;
self.options = $.extend({}, defaultOptions, options);
// When itemValue is set, freeInput should always be false
if (self.objectItems)
self.options.freeInput = false;
makeOptionItemFunction(self.options, 'itemValue');
makeOptionItemFunction(self.options, 'itemText');
makeOptionFunction(self.options, 'tagClass');
// Typeahead Bootstrap version 2.3.2
if (self.options.typeahead) {
var typeahead = self.options.typeahead || {};
makeOptionFunction(typeahead, 'source');
self.$input.typeahead($.extend({}, typeahead, {
source: function (query, process) {
function processItems(items) {
var texts = [];
for (var i = 0; i < items.length; i++) {
var text = self.options.itemText(items[i]);
map[text] = items[i];
texts.push(text);
}
process(texts);
}
this.map = {};
var map = this.map,
data = typeahead.source(query);
if ($.isFunction(data.success)) {
// support for Angular callbacks
data.success(processItems);
} else if ($.isFunction(data.then)) {
// support for Angular promises
data.then(processItems);
} else {
// support for functions and jquery promises
$.when(data)
.then(processItems);
}
},
updater: function (text) {
self.add(this.map[text]);
return this.map[text];
},
matcher: function (text) {
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
},
sorter: function (texts) {
return texts.sort();
},
highlighter: function (text) {
var regex = new RegExp( '(' + this.query + ')', 'gi' );
return text.replace( regex, "<strong>$1</strong>" );
}
}));
}
// typeahead.js
if (self.options.typeaheadjs) {
// Determine if main configurations were passed or simply a dataset
var typeaheadjs = self.options.typeaheadjs;
if (!$.isArray(typeaheadjs)) {
typeaheadjs = [null, typeaheadjs];
}
$.fn.typeahead.apply(self.$input, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum, name) {
var index = 0;
typeaheadjs.some(function(dataset, _index) {
if (dataset.name === name) {
index = _index;
return true;
}
return false;
});
// @TODO Dep: https://github.com/corejavascript/typeahead.js/issues/89
if (typeaheadjs[index].valueKey) {
self.add(datum[typeaheadjs[index].valueKey]);
} else {
self.add(datum);
}
self.$input.typeahead('val', '');
}, self));
}
self.$container.on('click', $.proxy(function(event) {
if (! self.$element.attr('disabled')) {
self.$input.removeAttr('disabled');
}
self.$input.focus();
}, self));
if (self.options.addOnBlur && self.options.freeInput) {
self.$input.on('focusout', $.proxy(function(event) {
// HACK: only process on focusout when no typeahead opened, to
// avoid adding the typeahead text as tag
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
self.add(self.$input.val());
self.$input.val('');
}
}, self));
}
// Toggle the 'focus' css class on the container when it has focus
self.$container.on({
focusin: function() {
self.$container.addClass(self.options.focusClass);
},
focusout: function() {
self.$container.removeClass(self.options.focusClass);
},
});
self.$container.on('keydown', 'input', $.proxy(function(event) {
var $input = $(event.target),
$inputWrapper = self.findInputWrapper();
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
switch (event.which) {
// BACKSPACE
case 8:
if (doGetCaretPosition($input[0]) === 0) {
var prev = $inputWrapper.prev();
if (prev.length) {
self.remove(prev.data('item'));
}
}
break;
// DELETE
case 46:
if (doGetCaretPosition($input[0]) === 0) {
var next = $inputWrapper.next();
if (next.length) {
self.remove(next.data('item'));
}
}
break;
// LEFT ARROW
case 37:
// Try to move the input before the previous tag
var $prevTag = $inputWrapper.prev();
if ($input.val().length === 0 && $prevTag[0]) {
$prevTag.before($inputWrapper);
$input.focus();
}
break;
// RIGHT ARROW
case 39:
// Try to move the input after the next tag
var $nextTag = $inputWrapper.next();
if ($input.val().length === 0 && $nextTag[0]) {
$nextTag.after($inputWrapper);
$input.focus();
}
break;
default:
// ignore
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, size));
}, self));
self.$container.on('keypress', 'input', $.proxy(function(event) {
var $input = $(event.target);
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
var text = $input.val(),
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
// Only attempt to add a tag if there is data in the field
if (text.length !== 0) {
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
$input.val('');
}
// If the field is empty, let the event triggered fire as usual
if (self.options.cancelConfirmKeysOnEmpty === false) {
event.preventDefault();
}
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, size));
}, self));
// Remove icon clicked
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
if (self.$element.attr('disabled')) {
return;
}
self.remove($(event.target).closest('.badge').data('item'));
}, self));
// Only add existing value as tags when using strings as tags
if (self.options.itemValue === defaultOptions.itemValue) {
if (self.$element[0].tagName === 'INPUT') {
self.add(self.$element.val());
} else {
$('option', self.$element).each(function() {
self.add($(this).attr('value'), true);
});
}
}
},
/**
* Removes all tagsinput behaviour and unregsiter all event handlers
*/
destroy: function() {
var self = this;
// Unbind events
self.$container.off('keypress', 'input');
self.$container.off('click', '[role=remove]');
self.$container.remove();
self.$element.removeData('tagsinput');
self.$element.show();
},
/**
* Sets focus on the tagsinput
*/
focus: function() {
this.$input.focus();
},
/**
* Returns the internal input element
*/
input: function() {
return this.$input;
},
/**
* Returns the element which is wrapped around the internal input. This
* is normally the $container, but typeahead.js moves the $input element.
*/
findInputWrapper: function() {
var elt = this.$input[0],
container = this.$container[0];
while(elt && elt.parentNode !== container)
elt = elt.parentNode;
return $(elt);
}
};
/**
* Register JQuery plugin
*/
$.fn.tagsinput = function(arg1, arg2, arg3) {
var results = [];
this.each(function() {
var tagsinput = $(this).data('tagsinput');
// Initialize a new tags input
if (!tagsinput) {
tagsinput = new TagsInput(this, arg1);
$(this).data('tagsinput', tagsinput);
results.push(tagsinput);
if (this.tagName === 'SELECT') {
$('option', $(this)).attr('selected', 'selected');
}
// Init tags from $(this).val()
$(this).val($(this).val());
} else if (!arg1 && !arg2) {
// tagsinput already exists
// no function, trying to init
results.push(tagsinput);
} else if(tagsinput[arg1] !== undefined) {
// Invoke function on existing tags input
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
var retVal = tagsinput[arg1](arg2, null, arg3);
}else{
var retVal = tagsinput[arg1](arg2);
}
if (retVal !== undefined)
results.push(retVal);
}
});
if ( typeof arg1 == 'string') {
// Return the results from the invoked function calls
return results.length > 1 ? results : results[0];
} else {
return results;
}
};
$.fn.tagsinput.Constructor = TagsInput;
/**
* Most options support both a string or number as well as a function as
* option value. This function makes sure that the option with the given
* key in the given options is wrapped in a function
*/
function makeOptionItemFunction(options, key) {
if (typeof options[key] !== 'function') {
var propertyName = options[key];
options[key] = function(item) { return item[propertyName]; };
}
}
function makeOptionFunction(options, key) {
if (typeof options[key] !== 'function') {
var value = options[key];
options[key] = function() { return value; };
}
}
/**
* HtmlEncodes the given value
*/
var htmlEncodeContainer = $('<div />');
function htmlEncode(value) {
if (value) {
return htmlEncodeContainer.text(value).html();
} else {
return '';
}
}
/**
* Returns the position of the caret in the given input field
* http://flightschool.acylt.com/devnotes/caret-position-woes/
*/
function doGetCaretPosition(oField) {
var iCaretPos = 0;
if (document.selection) {
oField.focus ();
var oSel = document.selection.createRange();
oSel.moveStart ('character', -oField.value.length);
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionStart;
}
return (iCaretPos);
}
/**
* Returns boolean indicates whether user has pressed an expected key combination.
* @param object keyPressEvent: JavaScript event object, refer
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
* @param object lookupList: expected key combinations, as in:
* [13, {which: 188, shiftKey: true}]
*/
function keyCombinationInList(keyPressEvent, lookupList) {
var found = false;
$.each(lookupList, function (index, keyCombination) {
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
found = true;
return false;
}
if (keyPressEvent.which === keyCombination.which) {
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
if (alt && shift && ctrl) {
found = true;
return false;
}
}
});
return found;
}
/**
* Initialize tagsinput behaviour on inputs and selects which have
* data-role=tagsinput
*/
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);

View file

@ -0,0 +1,55 @@
'use strict';
import "./lib/jquery.tristate"
class TristateHelper {
constructor() {
this.registerTriStateCheckboxes();
this.registerSubmitHandler();
}
registerSubmitHandler() {
document.addEventListener("turbo:submit-start", (e) => {
var form = e.detail.formSubmission.formElement;
var formData = e.detail.formSubmission.formData;
var $tristate_checkboxes = $('.tristate:checkbox', form);
//Iterate over each tristate checkbox in the form and set formData to the correct value
$tristate_checkboxes.each(function() {
var $checkbox = $(this);
var state = $checkbox.tristate('state');
formData.set($checkbox.attr('name'), state);
});
});
}
registerTriStateCheckboxes() {
//Initialize tristate checkboxes and if needed the multicheckbox functionality
const listener = () => {
$(".tristate").tristate( {
checked: "true",
unchecked: "false",
indeterminate: "indeterminate",
});
$('.permission_multicheckbox:checkbox').change(function() {
//Find the other checkboxes in this row, and change their value
var $row = $(this).parents('tr');
//@ts-ignore
var new_state = $(this).tristate('state');
//@ts-ignore
$('.tristate:checkbox', $row).tristate('state', new_state);
});
}
document.addEventListener("turbo:load", listener);
document.addEventListener("turbo:render", listener);
}
}
export default new TristateHelper();

View file

@ -1,604 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Extract the title (The name between the <title> tags) of a HTML snippet.
* @param {string} html The HTML code which should be searched.
* @returns {string} The title extracted from the html.
*/
function extractTitle(html : string) : string {
let title : string = "";
let regex = /<title>(.*?)<\/title>/gi;
if (regex.test(html)) {
let matches = html.match(regex);
for(let match in matches) {
title = $(matches[match]).text();
}
}
return title;
}
class AjaxUI {
protected BASE = "/";
private trees_filled : boolean = false;
private statePopped : boolean = false;
public xhr : XMLHttpRequest;
public constructor()
{
//Make back in the browser go back in history
window.onpopstate = this.onPopState;
$(document).ajaxError(this.onAjaxError.bind(this));
//$(document).ajaxComplete(this.onAjaxComplete.bind(this));
}
/**
* Starts the ajax ui und execute handlers registered in addStartAction().
* Should be called in a document.ready, after handlers are set.
*/
public start(disabled : boolean = false)
{
if(disabled) {
return;
}
console.info("AjaxUI started!");
this.BASE = $("body").data("base-url");
//If path doesn't end with slash, add it.
if(this.BASE[this.BASE.length - 1] !== '/') {
this.BASE = this.BASE + '/';
}
console.info("Base path is " + this.BASE);
//Show flash messages
$(".toast").toast('show');
/**
* Save the XMLHttpRequest that jQuery used, to the class, so we can acess the responseURL property.
* This is a work-around as long jQuery does not implement this property in its jQXHR objects.
*/
//@ts-ignore
jQuery.ajaxSettings.xhr = function () {
//@ts-ignore
let xhr = new window.XMLHttpRequest();
//Save the XMLHttpRequest to the class.
ajaxUI.xhr = xhr;
return xhr;
};
this.registerLinks();
this.registerForm();
this.fillTrees();
this.initDataTables();
//Trigger start event
$(document).trigger("ajaxUI:start");
}
/**
* Fill the trees with the given data.
*/
public fillTrees()
{
let categories = localStorage.getItem("tree_datasource_tree-categories");
let devices = localStorage.getItem("tree_datasource_tree-devices");
let tools = localStorage.getItem("tree_datasource_tree-tools");
if(categories == null) {
categories = "categories";
}
if(devices == null) {
devices = "devices";
}
if(tools == null) {
tools = "tools";
}
this.treeLoadDataSource("tree-categories", categories);
this.treeLoadDataSource("tree-devices", devices);
this.treeLoadDataSource("tree-tools", tools);
this.trees_filled = true;
//Register tree btns to expand all, or to switch datasource.
$(".tree-btns").click(function (event) {
event.preventDefault();
$(this).parents("div.dropdown").removeClass('show');
//$(this).closest(".dropdown-menu").removeClass('show');
$(".dropdown-menu.show").removeClass("show");
let mode = $(this).data("mode");
let target = $(this).data("target");
let text = $(this).text() + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
if (mode==="collapse") {
// @ts-ignore
$('#' + target).treeview('collapseAll', { silent: true });
}
else if(mode==="expand") {
// @ts-ignore
$('#' + target).treeview('expandAll', { silent: true });
} else {
localStorage.setItem("tree_datasource_" + target, mode);
ajaxUI.treeLoadDataSource(target, mode);
}
return false;
});
}
/**
* Load the given url into the tree with the given id.
* @param target_id
* @param datasource
*/
protected treeLoadDataSource(target_id, datasource) {
let text : string = $(".tree-btns[data-mode='" + datasource + "']").html();
text = text + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
switch(datasource) {
case "categories":
ajaxUI.initTree("#" + target_id, 'tree/categories');
break;
case "locations":
ajaxUI.initTree("#" + target_id, 'tree/locations');
break;
case "footprints":
ajaxUI.initTree("#" + target_id, 'tree/footprints');
break;
case "manufacturers":
ajaxUI.initTree("#" + target_id, 'tree/manufacturers');
break;
case "suppliers":
ajaxUI.initTree("#" + target_id, 'tree/suppliers');
break;
case "tools":
ajaxUI.initTree("#" + target_id, 'tree/tools');
break;
case "devices":
ajaxUI.initTree("#" + target_id, 'tree/devices');
break;
}
$( "#" + target_id + "-title").html(text);
}
/**
* Fill a treeview with data from the given url.
* @param tree The Jquery selector for the tree (e.g. "#tree-tools")
* @param url The url from where the data should be loaded
*/
public initTree(tree, url) {
//let contextmenu_handler = this.onNodeContextmenu;
$.getJSON(ajaxUI.BASE + url, function (data) {
// @ts-ignore
$(tree).treeview({
data: data,
enableLinks: false,
showIcon: false,
showBorder: true,
searchResultBackColor: '#ffc107',
searchResultColor: '#000',
onNodeSelected: function(event, data) {
if(data.href) {
ajaxUI.navigateTo(data.href);
}
},
//onNodeContextmenu: contextmenu_handler,
expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview"})
.on('initialized', function() {
$(this).treeview('collapseAll', { silent: true });
//Implement searching if needed.
if($(this).data('treeSearch')) {
let _this = this;
let $search = $($(this).data('treeSearch'));
$search.on( 'input', function() {
$(_this).treeview('collapseAll', { silent: true });
$(_this).treeview('search', [$search.val()]);
});
}
});
});
}
/**
* Register all links, for loading via ajax.
*/
public registerLinks()
{
// Unbind all old handlers, so the things are not executed multiple times.
$('a').not(".link-external, [data-no-ajax], .page-link, [href^='javascript'], [href^='#']").unbind('click').click(function (event) {
let a = $(this);
let href = $.trim(a.attr("href"));
//Ignore links without href attr and nav links ('they only have a #)
if(href != null && href != "" && href.charAt(0) !== '#') {
event.preventDefault();
ajaxUI.navigateTo(href);
}
}
);
console.debug('Links registered!');
}
protected getFormOptions() : JQueryFormOptions
{
return {
success: this.onAjaxComplete,
beforeSerialize: function($form, options) : boolean {
//Update the content of textarea fields using CKEDITOR before submitting.
//@ts-ignore
if(typeof CKEDITOR !== 'undefined') {
//@ts-ignore
for (let name in CKEDITOR.instances) {
//@ts-ignore
CKEDITOR.instances[name].updateElement();
}
}
//Check every checkbox field, so that it will be submitted (only valid fields are submitted)
$form.find("input[type=checkbox].tristate").prop('checked', true);
return true;
},
beforeSubmit: function (arr, $form, options) : boolean {
//When data-with-progbar is specified, then show progressbar.
if($form.data("with-progbar") != undefined) {
ajaxUI.showProgressBar();
}
return true;
}
};
}
/**
* Register all forms for loading via ajax.
*/
public registerForm()
{
let options = this.getFormOptions();
$('form').not('[data-no-ajax]').ajaxForm(options);
console.debug('Forms registered!');
}
/**
* Submits the given form via ajax.
* @param form The form that will be submmitted.
* @param btn The btn via which the form is submitted
*/
public submitForm(form, btn = null)
{
let options = ajaxUI.getFormOptions();
if(btn) {
options.data = {};
options.data[$(btn).attr('name')] = $(btn).attr('value');
}
$(form).ajaxSubmit(options);
}
/**
* Show the progressbar
*/
public showProgressBar()
{
//Blur content background
$('#content').addClass('loading-content');
// @ts-ignore
$('#progressModal').modal({
keyboard: false,
backdrop: false,
show: true
});
}
/**
* Hides the progressbar.
*/
public hideProgressBar()
{
// @ts-ignore
$('#progressModal').modal('hide');
//Remove the remaining things of the modal
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
$('body, .navbar').css('padding-right', "");
}
/**
* Navigates to the given URL
* @param url The url which should be opened.
* @param show_loading Show the loading bar during loading.
*/
public navigateTo(url : string, show_loading : boolean = true)
{
if(show_loading) {
this.showProgressBar();
}
$.ajax(url, {
success: this.onAjaxComplete
});
//$.ajax(url).promise().done(this.onAjaxComplete);
}
/**
* Called when an error occurs on loading ajax. Outputs the message to the console.
*/
private onAjaxError (event, request, settings) {
'use strict';
//Ignore aborted requests.
if (request.statusText =='abort' || request.status == 0) {
return;
}
//Ignore ajax errors with 200 code (like the ones during 2FA authentication)
if(request.status == 200) {
return;
}
console.error("Error getting the ajax data from server!");
console.log(event);
console.log(request);
console.log(settings);
ajaxUI.hideProgressBar();
//Create error text
let title = request.statusText;
switch(request.status) {
case 500:
title = 'Internal Server Error!';
break;
case 404:
title = "Site not found!";
break;
case 403:
title = "Permission denied!";
break;
}
var alert = bootbox.alert(
{
size: 'large',
message: function() {
let msg = "Error getting data from Server! <b>Status Code: " + request.status + "</b>";
msg += '<br><br><a class=\"btn btn-link\" data-toggle=\"collapse\" href=\"#iframe_div\" >' + 'Show response' + "</a>";
msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='iframe'></iframe></div>";
return msg;
},
title: title,
callback: function () {
//Remove blur
$('#content').removeClass('loading-content');
}
});
//@ts-ignore
alert.init(function (){
var dstFrame = document.getElementById('iframe');
//@ts-ignore
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
dstDoc.write(request.responseText);
dstDoc.close();
});
//If it was a server error and response is not empty, show it to user.
if(request.status == 500 && request.responseText !== "")
{
console.log("Response:" + request.responseText);
}
}
/**
* This function gets called every time, the "back" button in the browser is pressed.
* We use it to load the content from history stack via ajax and to rewrite url, so we only have
* to load #content-data
* @param event
*/
private onPopState(event)
{
let page : string = location.href;
ajaxUI.statePopped = true;
ajaxUI.navigateTo(page);
}
/**
* This function takes the response of an ajax requests, and does the things we need to do for our AjaxUI.
* This includes inserting the content and pushing history.
* @param responseText
* @param textStatus
* @param jqXHR
*/
private onAjaxComplete(responseText: string, textStatus: string, jqXHR: any)
{
console.debug("Ajax load completed!");
ajaxUI.hideProgressBar();
/* We need to do the url checking before the parseHTML, so that we dont get wrong url name, caused by scripts
in the new content */
// @ts-ignore
let url = this.url;
//Check if we were redirect to a new url, then we should use that as new url.
if(ajaxUI.xhr.responseURL) {
url = ajaxUI.xhr.responseURL;
}
//Parse response to DOM structure
//We need to preserve javascript, so the table ca
let dom = $.parseHTML(responseText, document, true);
//And replace the content container
$("#content").replaceWith($("#content", dom));
//Replace login menu too (so everything is up to date)
$("#login-content").replaceWith($('#login-content', dom));
//Replace flash messages and show them
$("#message-container").replaceWith($('#message-container', dom));
$(".toast").toast('show');
//Set new title
let title = extractTitle(responseText);
document.title = title;
//Push to history, if we currently arent poping an old value.
if(!ajaxUI.statePopped) {
history.pushState(null, title, url);
} else {
//Clear pop state
ajaxUI.statePopped = false;
}
//Do things on the new dom
ajaxUI.registerLinks();
ajaxUI.registerForm();
ajaxUI.initDataTables();
//Trigger reload event
$(document).trigger("ajaxUI:reload");
}
/**
* Init all datatables marked with data-datatable based on their data-settings attribute.
*/
protected initDataTables()
{
//@ts-ignore
$($.fn.DataTable.tables()).DataTable().fixedHeader.disable();
//@ts-ignore
$($.fn.DataTable.tables()).DataTable().destroy();
//Find all datatables and init it.
let $tables = $('[data-datatable]');
$.each($tables, function(index, table) {
let $table = $(table);
let settings = $table.data('settings');
//@ts-ignore
var promise = $('#part_list').initDataTables(settings,
{
colReorder: true,
responsive: true,
"fixedHeader": { header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
headerOffset: $("#navbar").height()},
"buttons": [ {
"extend": 'colvis',
'className': 'mr-2 btn-light',
"text": "<i class='fa fa-cog'></i>"
}],
"select": $table.data('select') ?? false,
"rowCallback": function( row, data, index ) {
//Check if we have a level, then change color of this row
if (data.level) {
let style = "";
switch(data.level) {
case "emergency":
case "alert":
case "critical":
case "error":
style = "table-danger";
break;
case "warning":
style = "table-warning";
break;
case "notice":
style = "table-info";
break;
}
if (style){
$(row).addClass(style);
}
}
}
});
//Register links.
promise.then(function() {
ajaxUI.registerLinks();
//Set the correct title in the table.
let title = $('#part-card-header-src');
$('#part-card-header').html(title.html());
$(document).trigger('ajaxUI:dt_loaded');
if($table.data('part_table')) {
//@ts-ignore
$('#dt').on( 'select.dt deselect.dt', function ( e, dt, items ) {
let selected_elements = dt.rows({selected: true});
let count = selected_elements.count();
if(count > 0) {
$('#select_panel').removeClass('d-none');
} else {
$('#select_panel').addClass('d-none');
}
$('#select_count').text(count);
let selected_ids_string = selected_elements.data().map(function(value, index) {
return value['id']; }
).join(",");
$('#select_ids').val(selected_ids_string);
} );
}
//Attach event listener to update links after new page selection:
$('#dt').on('draw.dt column-visibility.dt', function() {
ajaxUI.registerLinks();
$(document).trigger('ajaxUI:dt_loaded');
});
});
});
console.debug('Datatables inited.');
}
}
export let ajaxUI = new AjaxUI();

View file

@ -1,671 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {ajaxUI} from "./ajax_ui";
import "bootbox";
import "marked";
import * as marked from "marked";
import {parse} from "marked";
import * as ZXing from "@zxing/library";
/************************************
*
* In this file all the functions that has to be called using AjaxUIoperation are registered.
* You can use AjaxUI:start and AjaxUI:reload events.
*
***********************************/
//Register greek input in search fields.
$(document).on("ajaxUI:start ajaxUI:reload", function() {
//@ts-ignore
$("input[type=text], textarea, input[type=search]").unbind("keydown").keydown(function (event : KeyboardEvent) {
let greek = event.altKey;
let greek_char : string = "";
if (greek){
switch(event.key) {
case "w": //Omega
greek_char = '\u2126';
break;
case "u":
case "m": //Micro
greek_char = "\u00B5";
break;
case "p": //Phi
greek_char = "\u03C6";
break;
case "a": //Alpha
greek_char = "\u03B1";
break;
case "b": //Beta
greek_char = "\u03B2";
break;
case "c": //Gamma
greek_char = "\u03B3";
break;
case "d": //Delta
greek_char = "\u03B4";
break;
case "l": //Pound
greek_char = "\u00A3";
break;
case "y": //Yen
greek_char = "\u00A5";
break;
case "o": //Yen
greek_char = "\u00A4";
break;
case "1": //Sum symbol
greek_char = "\u2211";
break;
case "2": //Integral
greek_char = "\u222B";
break;
case "3": //Less-than or equal
greek_char = "\u2264";
break;
case "4": //Greater than or equal
greek_char = "\u2265";
break;
case "5": //PI
greek_char = "\u03c0";
break;
case "q": //Copyright
greek_char = "\u00A9";
break;
case "e": //Euro
greek_char = "\u20AC";
break;
}
if(greek_char=="") return;
let $txt = $(this);
//@ts-ignore
let caretPos = $txt[0].selectionStart;
let textAreaTxt = $txt.val().toString();
$txt.val(textAreaTxt.substring(0, caretPos) + greek_char + textAreaTxt.substring(caretPos) );
}
});
//@ts-ignore
this.greek_once = true;
});
//Register bootstrap select picker
$(document).on("ajaxUI:reload ajaxUI:start", function () {
//@ts-ignore
$(".selectpicker").selectpicker({
dropdownAlignRight: 'auto',
container: '#content',
});
});
//Use bootstrap tooltips for the most tooltips
$(document).on("ajaxUI:start ajaxUI:reload ajaxUI:dt_loaded", function () {
$(".tooltip").remove();
$('a[title], button[title], span[title], h6[title], h3[title], i.fas[title]')
//@ts-ignore
.tooltip("hide").tooltip({container: "body", placement: "auto", boundary: 'window'});
});
// Add bootstrap treeview on divs with data-tree-data attribute
$(document).on("ajaxUI:start ajaxUI:reload", function() {
$("[data-tree-data]").each(function(index, element) {
let data = $(element).data('treeData');
//@ts-ignore
$(element).treeview({
data: data,
enableLinks: false,
showIcon: false,
showBorder: true,
searchResultBackColor: '#ffc107',
searchResultColor: '#000',
showTags: true,
//@ts-ignore
wrapNode: true,
//@ts-ignore
tagsClass: 'badge badge-secondary badge-pill pull-right',
expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview",
onNodeSelected: function(event, data) {
if(data.href) {
ajaxUI.navigateTo(data.href);
}
}
}).on('initialized', function() {
$(this).treeview('collapseAll', { silent: true });
let selected = $(this).treeview('getSelected');
$(this).treeview('revealNode', [ selected, {silent: true } ]);
//Implement searching if needed.
if($(this).data('treeSearch')) {
let _this = this;
let $search = $($(this).data('treeSearch'));
$search.on( 'input', function() {
$(_this).treeview('collapseAll', { silent: true });
$(_this).treeview('search', [$search.val()]);
});
}
//Add tree expand and reduce buttons if needed.
if($(this).data('treeReduce')) {
let _this = this;
let $btn = $($(this).data('treeReduce'));
$btn.click(function () {
$(_this).treeview('collapseAll');
});
}
if($(this).data('treeExpand')) {
let _this = this;
let $btn = $($(this).data('treeExpand'));
$btn.click(function () {
$(_this).treeview('expandAll');
});
}
});
});
});
$(document).on("ajaxUI:start ajaxUI:reload", function() {
$("[data-delete-form]").unbind('submit').submit(function (event) {
event.preventDefault();
let form = this;
//Get the submit button
let btn = document.activeElement;
let title = $(this).data("title");
let message = $(this).data("message");
bootbox.confirm({
message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form.
if (result) {
ajaxUI.submitForm(form, btn);
}
}
});
return false;
});
//Register for forms with delete-buttons
$("[data-delete-btn]").parents('form').unbind('submit').submit(function (event) {
event.preventDefault();
let form = this;
//Get the submit button
let btn = document.activeElement;
let title = $(btn).data("title");
let message = $(btn).data("message");
//If not the button with the message was pressed, then simply submit the form.
if(!btn.hasAttribute('data-delete-btn')) {
ajaxUI.submitForm(form, btn);
}
bootbox.confirm({
message: message, title: title, callback: function (result) {
//If the dialog was confirmed, then submit the form.
if (result) {
ajaxUI.submitForm(form, btn);
}
}
});
});
});
$(document).on("ajaxUI:start ajaxUI:reload", function() {
//@ts-ignore
$(".tristate").tristate( {
checked: "true",
unchecked: "false",
indeterminate: "indeterminate",
});
$('.permission_multicheckbox:checkbox').change(function() {
//Find the other checkboxes in this row, and change their value
var $row = $(this).parents('tr');
//@ts-ignore
var new_state = $(this).tristate('state');
//@ts-ignore
$('.tristate:checkbox', $row).tristate('state', new_state);
});
});
//Re initialize fileinputs on reload
$(document).on("ajaxUI:reload", function () {
//@ts-ignore
$(".file").fileinput();
});
$(document).on("ajaxUI:start ajaxUI:reload", function () {
$('input.tagsinput').each(function() {
//Use typeahead if an autocomplete url was specified.
if($(this).data('autocomplete')) {
//@ts-ignore
var engine = new Bloodhound({
//@ts-ignore
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
//@ts-ignore
queryTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
remote: {
url: $(this).data('autocomplete'),
wildcard: 'QUERY'
}
});
//@ts-ignore
$(this).tagsinput({
typeaheadjs: {
name: 'tags',
source: engine.ttAdapter()
}
});
} else { //Init tagsinput without typeahead
//@ts-ignore
$(this).tagsinput();
}
})
});
/**
* Register the button, to jump to the top of the page.
*/
$(document).on("ajaxUI:start", function registerJumpToTop() {
$(window).scroll(function () {
if ($(this).scrollTop() > 50) {
$('#back-to-top').fadeIn();
} else {
$('#back-to-top').fadeOut();
}
});
// scroll body to 0px on click
$('#back-to-top').click(function () {
$('#back-to-top').tooltip('hide');
$('body,html').animate({
scrollTop: 0
}, 800);
return false;
}).tooltip();
});
/**
* This listener keeps track of which tab is currently selected (using hash and localstorage) and will try to open
* that tab on reload. That means that if the user changes something, he does not have to switch back to the tab
* where he was before submit.
*/
$(document).on("ajaxUI:reload ajaxUI:start", function () {
//Determine which tab should be shown (use hash if specified, otherwise use localstorage)
var $activeTab = null;
if (location.hash) {
$activeTab = $('a[href=\'' + location.hash + '\']');
} else if(localStorage.getItem('activeTab')) {
$activeTab = $('a[href="' + localStorage.getItem('activeTab') + '"]');
}
if($activeTab) {
//Findout if the tab has any parent tab we have to show before
var parents = $($activeTab).parents('.tab-pane');
parents.each(function(n) {
$('a[href="#' + $(this).attr('id') + '"]').tab('show');
});
//Finally show the active tab itself
$activeTab.tab('show');
}
$('body').on('click', 'a[data-toggle=\'tab\']', function (e) {
e.preventDefault()
var tab_name = this.getAttribute('href')
if (history.replaceState) {
history.replaceState(null, null, tab_name)
}
else {
location.hash = tab_name
}
localStorage.setItem('activeTab', tab_name)
$(this).tab('show');
return false;
});
});
/**
* Load the higher resolution version of hover pictures.
*/
$(document).on("ajaxUI:reload ajaxUI:start ajaxUI:dt_loaded", function () {
$('.hoverpic[data-thumbnail]').popover({
html: true,
trigger: 'hover',
placement: 'right',
container: 'body',
content: function () {
return '<img class="img-fluid" src="' + $(this).data('thumbnail') + '" />';
}
});
});
/*
* Register the button which is used to
*/
$(document).on("ajaxUI:start", function() {
let $sidebar = $("#fixed-sidebar");
let $container = $("#main");
let $toggler = $('#sidebar-toggle-button');
function sidebarHide() {
$sidebar.hide();
$container.removeClass('col-md-9 col-lg-10 offset-md-3 offset-lg-2');
$container.addClass('col-12');
$toggler.html('<i class="fas fa-angle-right"></i>');
$toggler.data('hidden', true);
localStorage.setItem('sidebarHidden', 'true');
}
function sidebarShow() {
let $sidebar = $("#fixed-sidebar");
$sidebar.show();
let $container = $("#main");
$container.removeClass('col-12');
$container.addClass('col-md-9 col-lg-10 offset-md-3 offset-lg-2');
$toggler.html('<i class="fas fa-angle-left"></i>');
$toggler.data('hidden', false);
localStorage.setItem('sidebarHidden', 'false');
}
//Make the state persistent over reloads
if(localStorage.getItem('sidebarHidden') === 'true') {
sidebarHide();
}
//Register handler
$toggler.click(function() {
if($(this).data('hidden')) {
sidebarShow();
} else {
sidebarHide();
}
});
});
//Register typeaheads
$(document).on("ajaxUI:reload ajaxUI:start attachment:create", function () {
$('input[data-autocomplete]').each(function() {
//@ts-ignore
var engine = new Bloodhound({
//@ts-ignore
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
//@ts-ignore
queryTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
remote: {
url: $(this).data('autocomplete'),
wildcard: 'QUERY'
}
});
//@ts-ignore
$(this).typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
name: 'states',
source: engine,
limit: 250,
templates: {
suggestion: function(data) {
if (typeof data === "string") {
return "<div>" + data + "</div>";
} else if(typeof data === "object" && typeof data.image === "string") {
return "<div class='row m-0'><div class='col-2 pl-0 pr-1'><img class='typeahead-image' src='" + data.image + "'/></div><div class='col-10'>" + data.name + "</div></div>"
}
},
},
display: 'name',
});
//Make the typeahead input fill the container (remove block-inline attr)
$(this).parent(".twitter-typeahead").css('display', 'block');
})
});
$(document).on("ajaxUI:start", function () {
function decodeHTML(html) {
var txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
}
function parseMarkdown() {
$('.markdown').each(function() {
let unescaped = marked(decodeHTML( $(this).data('markdown')));
//@ts-ignore
let escaped = DOMPurify.sanitize(unescaped);
$(this).html(escaped);
//Remove markdown from DOM
$(this).removeAttr('data-markdown');
//Make all links external
$('a', this).addClass('link-external').attr('target', '_blank').attr('rel', 'noopener');
//Bootstrapify objects
$('table', this).addClass('table table-hover table-striped table-bordered');
});
//Latex rendering have to be done after markdown parsing
$('.latex').each(function(index, element) {
//@ts-ignore
window.renderMathInElement(element, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false},
{left: "\\[", right: "\\]", display: true}
]
});
});
}
//Configure markdown
marked.setOptions({
gfm: true,
});
parseMarkdown();
$(document).on("ajaxUI:reload", parseMarkdown);
$(document).on("ajaxUI:dt_loaded", parseMarkdown);
});
$(document).on("ajaxUI:start ajaxUI:reload attachment:create", function() {
let updater = function() {
//@ts-ignore
let selected_option = $(this)[0].selectedOptions[0];
let filter_string = $(selected_option).data('filetype_filter');
//Find associated file input
let $row = $(this).parents('tr');
//Set accept filter
$('input[type="file"]', $row).prop('accept', filter_string);
};
//Register a change handler on all change listeners, and update it when the events are triggered
$('select.attachment_type_selector').change(updater).each(updater);
});
$(document).on("ajaxUI:start ajaxUI:reload", function() {
function setTooltip(btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
}
function hideTooltip(btn) {
setTimeout(function() {
$(btn).tooltip('hide');
}, 1000);
}
//@ts-ignore
var clipboard = new ClipboardJS('.btn[data-clipboard-target], .btn[data-clipboard-text], .btn[data-clipboard-action]');
clipboard.on('success', function(e) {
setTooltip(e.trigger, 'Copied!');
hideTooltip(e.trigger);
});
clipboard.on('error', function(e) {
setTooltip(e.trigger, 'Failed!');
hideTooltip(e.trigger);
});
});
//Register U2F on page reload too...
$(document).on("ajaxUI:reload", function() {
//@ts-ignore
window.u2fauth.ready(function () {
const form = document.getElementById('u2fForm')
if (!form) {
return
}
const type = form.dataset.action
if (type === 'auth') {
//@ts-ignore
u2fauth.authenticate()
} else if (type === 'reg' && form.addEventListener) {
form.addEventListener('submit', function (event) {
event.preventDefault()
//@ts-ignore
u2fauth.register()
}, false)
}
})
});
//Reuse codereader between multiple requests
const codeReader = new ZXing.BrowserMultiFormatReader();
//Init barcode scanner
$(document).on("ajaxUI:start ajaxUI:reload", function() {
//Skip if we are not on scanner page...
if (!document.getElementById('scan_dialog_form')) {
codeReader.reset();
return;
}
let selectedDeviceId;
//Save it for later, so we can reset it
console.log('ZXing code reader initialized');
codeReader.listVideoInputDevices()
.then((videoInputDevices) => {
if (videoInputDevices.length >= 1) {
const sourceSelect = document.getElementById('sourceSelect');
videoInputDevices.forEach((element) => {
const sourceOption = document.createElement('option');
sourceOption.text = element.label;
sourceOption.value = element.deviceId;
sourceSelect.appendChild(sourceOption);
});
//Try to retrieve last selected webcam...
let last_cam_id = localStorage.getItem('scanner_last_cam_id');
if (!!last_cam_id) {
//selectedDeviceId = localStorage.getItem('scanner_last_cam_id');
$(sourceSelect).val(last_cam_id);
} else {
selectedDeviceId = videoInputDevices[0].deviceId;
}
sourceSelect.onchange = () => {
//@ts-ignore
selectedDeviceId = sourceSelect.value;
localStorage.setItem('scanner_last_cam_id', selectedDeviceId);
changeHandler();
};
document.getElementById('sourceSelectPanel').classList.remove('d-none');
document.getElementById('video').classList.remove('d-none');
document.getElementById('scanner-warning').classList.add('d-none');
}
let changeHandler = () => {
codeReader.reset();
codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => {
if (result) {
//@ts-ignore
document.getElementById('scan_dialog_input').value = result.text;
//Submit form
//@ts-ignore
document.getElementById('scan_dialog_form').submit();
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error(err);
//document.getElementById('result').textContent = err
}
});
console.log(`Started continous decode from camera with id ${selectedDeviceId}`)
};
//Register Change Src Button
//document.getElementById('changeSrcBtn').addEventListener('click', changeHandler);
//Try to start logging automatically.
changeHandler();
})
.catch((err) => {
console.error(err)
})
});
//Need for proper body padding, with every navbar height
$(window).resize(function () {
let height : number = $('#navbar').height() + 10;
$('body').css('padding-top', height);
$('#fixed-sidebar').css('top', height);
});
$(window).on('load', function () {
let height : number = $('#navbar').height() + 10;
$('body').css('padding-top', height);
$('#fixed-sidebar').css('top', height);
});

View file

@ -20,7 +20,6 @@
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
"florianv/swap-bundle": "dev-master",
"friendsofsymfony/ckeditor-bundle": "^2.0",
"gregwar/captcha-bundle": "^2.1.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
@ -56,6 +55,7 @@
"symfony/serializer": "5.4.*",
"symfony/translation": "5.4.*",
"symfony/twig-bundle": "5.4.*",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "5.4.*",
"symfony/web-link": "5.4.*",
"symfony/webpack-encore-bundle": "^1.1",
@ -121,9 +121,7 @@
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"ckeditor:install --clear=drop": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"ckeditor:install": "symfony-cmd"
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"

814
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,6 @@ return [
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Omines\DataTablesBundle\DataTablesBundle::class => ['all' => true],
Shivas\VersioningBundle\ShivasVersioningBundle::class => ['all' => true],
FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true],
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
@ -25,4 +24,5 @@ return [
Translation\Bundle\TranslationBundle::class => ['all' => true],
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
];

View file

@ -15,7 +15,8 @@ doctrine:
class: App\Helpers\BigDecimalType
schema_filter: ~^(?!internal)~
profiling_collect_backtrace: true
# Only enable this when needed
profiling_collect_backtrace: false
orm:
auto_generate_proxy_classes: true

View file

@ -1,79 +0,0 @@
# Read the documentation: https://symfony.com/doc/current/bundles/FOSCKEditorBundle/index.html
twig:
form_themes:
- '@FOSCKEditor/Form/ckeditor_widget.html.twig'
fos_ck_editor:
default_config: complex_config
configs:
complex_config:
extraPlugins: "markdown"
toolbar: comment_toolbar
simple_config:
extraPlugins: "markdown"
toolbar: basic
description_config:
extraPlugins: "markdown"
height: 60
toolbar: description_toolbar
label_config:
height: 100
enterMode: 2
toolbar: label_toolbar
extraPlugins: ["partdb_label", "showprotected"]
allowedContent: true
font_names: >
DejaVu Sans Mono/DejaVu Sans Mono;
DejaVu Sans/DejaVu Sans;
DejaVu Serif/DejaVu Serif;
Helvetica/Helvetica, Arial, sans-serif;
Times New Roman/Times New Roman, Times, serif;
Courier New/Courier New, Courier, monospace;
plugins:
bbcode:
path: "ckeditor/plugins/bbcode/" # with trailing slash
filename: "plugin.js"
markdown:
path: "ckeditor/plugins/markdown/"
filename: "plugin.js"
specialchar:
path: "ckeditor/plugins/specialchar"
filename: "plugin.js"
partdb_label:
path: "ckeditor/plugins/partdb_label/"
filename: "plugin.js"
showprotected:
path: "ckeditor/plugins/showprotected/"
filename: "plugin.js"
toolbars:
configs:
label_toolbar:
- [ 'Bold', 'Italic', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
- ['JustifyLeft', 'JustifyCenter', 'JustifyRight']
- ["SpecialChar"]
- ["Source"]
- "/"
- ['Format', 'FontSize', 'Font']
- ['Table', 'HorizontalRule']
- ['Placeholders']
description_toolbar:
- [ 'Bold', 'Italic', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
- ["SpecialChar"]
- ["Source"]
comment_toolbar:
- ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']
- [ 'Find', 'Replace', '-', 'SelectAll', '-', 'Scayt' ]
- [ 'Styles', 'Format']
- [ 'TextColor']
- [ 'Maximize', 'ShowBlocks' ]
- [ 'Source', '-', 'About']
- '/'
- [ 'Bold', 'Italic', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
- [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock']
- [ 'Link', 'Unlink' ]
- [ 'Image', 'Table', 'HorizontalRule', 'SpecialChar' ]

View file

@ -29,3 +29,32 @@ nelmio_security:
policies:
- 'no-referrer'
- 'strict-origin-when-cross-origin'
csp:
enabled: true
hosts: [ ]
content_types: [ ]
enforce:
level1_fallback: false
browser_adaptive:
enabled: false
report-uri: '%router.request_context.base_url%/csp/report'
default-src:
- 'self'
img-src:
- '*'
- 'data:'
style-src:
- 'self'
- 'unsafe-inline'
- 'data:'
script-src:
- 'self'
object-src:
- 'self'
- 'data:'
frame-src:
- 'self'
- 'data:'
block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport
# upgrade-insecure-requests: true # defaults to false, upgrades HTTP requests to HTTPS transport

View file

@ -1,6 +1,6 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['bootstrap_4_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ]
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ]
paths:
'%kernel.project_dir%/assets/css': css

View file

@ -9,10 +9,10 @@ webpack_encore:
defer: true
# Uncomment (also under link_attributes) if using Turbo Drive
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
# 'data-turbo-track': reload
# link_attributes:
'data-turbo-track': reload
link_attributes:
# Uncomment if using Turbo Drive
# 'data-turbo-track': reload
'data-turbo-track': reload
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
# crossorigin: 'anonymous'

View file

@ -8,6 +8,12 @@ scan_qr:
path: /scan/{type}/{id}
controller: App\Controller\ScanController:scanQRCode
csp_report:
path: /csp/report
methods: [POST]
defaults: { _controller: nelmio_security.csp_reporter_controller::indexAction }
# Must be last as it matches everything
redirector:
path: /{url}
requirements:

View file

@ -1,17 +1,19 @@
{
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.7.2",
"@fortawesome/fontawesome-free": "^6.1.1",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/turbo": "^7.0.1",
"@popperjs/core": "^2.10.2",
"@symfony/stimulus-bridge": "^3.0.0",
"@symfony/webpack-encore": "^1.6.1",
"bootstrap": "^4.4.1",
"core-js": "^3.6.4",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets",
"@symfony/webpack-encore": "^3.0.0",
"bootstrap": "^5.1.3",
"core-js": "^3.23.0",
"jquery": "^3.5.1",
"popper.js": "^1.14.7",
"regenerator-runtime": "^0.13.2",
"regenerator-runtime": "^0.13.9",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-notifier": "^1.6.0"
"webpack-notifier": "^1.15.0"
},
"license": "AGPL-3.0-or-later",
"private": true,
@ -22,38 +24,64 @@
"build": "encore production --progress"
},
"dependencies": {
"@types/bootbox": "^5.2.0",
"@types/bootstrap": "^4.3.0",
"@types/bootstrap-treeview": "^1.20.0",
"@types/jquery": "^3.3.29",
"@types/jquery.form": "^3.26.30",
"@types/marked": "^3.0.1",
"@types/typeahead": "^0.11.32",
"@ckeditor/ckeditor5-alignment": "^34.2.0",
"@ckeditor/ckeditor5-autoformat": "^34.2.0",
"@ckeditor/ckeditor5-basic-styles": "^34.2.0",
"@ckeditor/ckeditor5-block-quote": "^34.2.0",
"@ckeditor/ckeditor5-code-block": "^34.2.0",
"@ckeditor/ckeditor5-dev-utils": "^30.3.1",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^30.3.1",
"@ckeditor/ckeditor5-editor-classic": "^34.2.0",
"@ckeditor/ckeditor5-essentials": "^34.2.0",
"@ckeditor/ckeditor5-find-and-replace": "^34.2.0",
"@ckeditor/ckeditor5-font": "^34.2.0",
"@ckeditor/ckeditor5-heading": "^34.2.0",
"@ckeditor/ckeditor5-highlight": "^34.2.0",
"@ckeditor/ckeditor5-horizontal-line": "^34.2.0",
"@ckeditor/ckeditor5-html-embed": "^34.2.0",
"@ckeditor/ckeditor5-html-support": "^34.2.0",
"@ckeditor/ckeditor5-image": "^34.2.0",
"@ckeditor/ckeditor5-indent": "^34.2.0",
"@ckeditor/ckeditor5-link": "^34.2.0",
"@ckeditor/ckeditor5-list": "^34.2.0",
"@ckeditor/ckeditor5-markdown-gfm": "^34.2.0",
"@ckeditor/ckeditor5-media-embed": "^34.2.0",
"@ckeditor/ckeditor5-paragraph": "^34.2.0",
"@ckeditor/ckeditor5-paste-from-office": "^34.2.0",
"@ckeditor/ckeditor5-remove-format": "^34.2.0",
"@ckeditor/ckeditor5-source-editing": "^34.2.0",
"@ckeditor/ckeditor5-special-characters": "^34.2.0",
"@ckeditor/ckeditor5-table": "^34.2.0",
"@ckeditor/ckeditor5-theme-lark": "^34.2.0",
"@ckeditor/ckeditor5-upload": "^34.2.0",
"@ckeditor/ckeditor5-watchdog": "^34.2.0",
"@ckeditor/ckeditor5-word-count": "^34.2.0",
"@zxing/library": "^0.19.1",
"bootbox": "^5.4.0",
"bootstrap-fileinput": "^5.0.1",
"bootstrap-select": "^1.13.8",
"bootswatch": "^4.3.1",
"bootstrap-select": "v1.14.0-beta3",
"bootswatch": "^5.1.3",
"bs-custom-file-input": "^1.3.4",
"clipboard": "^2.0.4",
"compression-webpack-plugin": "^9.0.0",
"copy-webpack-plugin": "^9.0.0",
"compression-webpack-plugin": "^10.0.0",
"copy-webpack-plugin": "^11.0.0",
"corejs-typeahead": "^1.2.1",
"darkmode-js": "^1.5.0",
"datatables.net-bs4": "^2.0.1",
"datatables.net-buttons-bs4": "^2.2.2",
"datatables.net-colreorder-bs4": "^1.5.1",
"datatables.net-fixedheader-bs4": "^3.1.5",
"datatables.net-responsive-bs4": "^2.2.3",
"datatables.net-select-bs4": "^1.2.7",
"datatables.net-bs5": "^1.10.20",
"datatables.net-buttons-bs5": "^2.2.2",
"datatables.net-colreorder-bs5": "^1.5.1",
"datatables.net-fixedheader-bs5": "^3.1.5",
"datatables.net-responsive-bs5": "^2.2.3",
"datatables.net-select-bs5": "^1.2.7",
"dompurify": "^2.0.6",
"emoji.json": "^13.1.0",
"exports-loader": "^3.0.0",
"jquery-form": "^4.2.2",
"jszip": "^3.2.0",
"katex": "^0.13.0",
"marked": "^3.0.1",
"katex": "^0.16.0",
"marked": "^4.0.3",
"patternfly-bootstrap-treeview": "^2.1.8",
"pdfmake": "^0.2.2",
"stimulus-use": "^0.50.0",
"tom-select": "^2.1.0",
"ts-loader": "^9.2.6",
"typescript": "^4.0.2",
"u2f-api": "^1.1.1"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,158 +0,0 @@
/**
* A markdown output plugin for CKEDITOR.
* Uses showdown.js for conversion and DOMPurifier for HTML filtering.
*
* This software is licensed under MIT License.
*
* Copyright (c) 2019 Jan Böhmer
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
(function() {
function overrideDataProcessor(editor)
{
//Both showdown and DOMPurify must be loaded
if(typeof(showdown) == 'undefined') return;
if (typeof(DOMPurify) == 'undefined') return;
//If the dataprocessor were already be overriden do nothing
if(editor.dataProcessor.markdown) return;
var converter = new showdown.Converter();
//Set some useful options on Showdown
converter.setFlavor('github');
converter.setOption('tables', true);
converter.setOption('strikethrough', true);
converter.setOption('parseImgDimensions', true);
converter.setOption('smartIndentationFix', true);
editor.dataProcessor = {
toDataFormat: function(html, fixForBody) {
//html = html.replace(/(\r\n|\n|\r)/gm,"");
html = html.replace('<br>', '\n');
//Support for strikethrough in markdown
html = html.replace('<s>', '<del>').replace('</s>', '</del>');
return converter.makeMarkdown(html);
},
toHtml: function(data){
//Convert markdown to HTML and remove unsafe things from it.
var unsafe = converter.makeHtml(data);
return DOMPurify.sanitize(unsafe);
},
//Mark this dataprocessor
markdown: true
};
//Set the original data
if(editor.markdown_unchangedData) {
editor.setData(editor.markdown_unchangedData);
editor.markdown_unchangedData = null;
}
}
CKEDITOR.plugins.add( 'markdown', {
//requires: 'entities',
onLoad: function() {
CKEDITOR.addCss(
//Show borders on tables generated by Showdown
'table {\n' +
' border-width: 1px 0 0 1px;\n' +
' border-color: #bbb;\n' +
' border-style: solid;\n' +
' }\n' +
'\n' +
'table td, table th {\n' +
' border-width: 0 1px 1px 0;\n' +
' border-color: #bbb;\n' +
' border-style: solid;\n' +
' padding: 10px;\n' +
'}' +
//Show code blocks
'pre {\n' +
' display: block;\n' +
' padding: 9.5px;\n' +
' margin: 0 0 10px;\n' +
' font-size: 13px;\n' +
' line-height: 1.42857143;\n' +
' color: #333;\n' +
' word-break: break-all;\n' +
' word-wrap: break-word;\n' +
' background-color: #f5f5f5;\n' +
' border: 1px solid #ccc;\n' +
' border-radius: 4px;\n' +
'}' +
'padding: 0;\n' +
' font-size: inherit;\n' +
' color: inherit;\n' +
' white-space: pre-wrap;\n' +
' background-color: transparent;\n' +
' border-radius: 0;' +
'code, kbd, pre, samp {\n' +
' font-family: Menlo, Monaco, Consolas, "Courier New", monospace;\n' +
'}' +
//Show images small
'img {\n' +
' max-width: 35%;\n' +
' vertical-align: middle;' +
'}'
);
},
beforeInit: function( editor ) {
var config = editor.config;
CKEDITOR.tools.extend( config, {
basicEntities: false,
entities: false,
fillEmptyBlocks: false
}, true );
editor.filter.disable();
},
init: function( editor ) {
var config = editor.config;
var rootPath = this.path;
//We override the dataprocessor later (after we loaded the needes scripts), sow we need the save untouched data now.
editor.markdown_unchangedData = editor.getData();
if (typeof(showdown) == 'undefined') {
CKEDITOR.scriptLoader.load(rootPath + 'js/showdown.min.js', function() {
overrideDataProcessor(editor);
}, CKEDITOR, true);
}
if (typeof(DOMPurify) == 'undefined') {
CKEDITOR.scriptLoader.load(rootPath + 'js/purify.min.js', function() {
overrideDataProcessor()
}, CKEDITOR, true);
}
},
afterInit: function( editor ) {
//Override the data processor with our for Markdown.
overrideDataProcessor(editor);
}
} );
} )();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,69 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
CKEDITOR.plugins.setLang( 'partdb_label', 'de', {
title: 'Platzhalter einfügen',
label: 'Platzhalter',
'section.global': 'Global',
'section.part': 'Bauteil',
'section.part_lot': 'Bauteilbestand',
'section.storelocation': 'Lagerort',
'part.id': 'Datenbank ID',
'part.name': 'Bauteilename',
'part.category': 'Kategorie',
'part.category_full': 'Kategorie (Ganzer Pfad)',
'part.manufacturer': 'Hersteller',
'part.manufacturer_full': 'Hersteller (Ganzer Pfad)',
'part.footprint': 'Footprint',
'part.footprint_full': 'Footprint (Ganzer Pfad)',
'part.mass': 'Gewicht',
'part.tags': 'Tags',
'part.mpn': 'Herstellernummer (MPN)',
'part.status': 'Herstellungsstatus',
'part.description': 'Beschreibung',
'part.description_t': 'Beschreibung (Text)',
'part.comment': 'Kommentar',
'part.comment_t': 'Kommentar (Text)',
'part.last_modified': 'Änderungsdatum',
'part.creation_date': 'Erstellungsdatum',
'global.username': 'Benutzername',
'global.username_full': 'Benutzername (inklusive Name)',
'global.datetime': 'Datum & Uhrzeit',
'global.date': 'Datum',
'global.time': 'Uhrzeit',
'global.install_name': 'Installationsname',
'global.type': 'Zieltyp',
'lot.id': 'Lot ID',
'lot.name': 'Lot Name',
'lot.comment': 'Lot Kommentar',
'lot.expiration_date': 'Ablaufdatum',
'lot.amount': 'Bestandsmenge',
'lot.location': 'Lagerort',
'lot.location_full': 'Lagerort (Ganzer Pfad)',
'storelocation.id': 'Lagerort ID',
'storelocation.name': 'Name',
'storelocation.full_path': 'Ganzer Pfad',
'storelocation.parent_name': 'Name des Übergeordneten Elements',
'storelocation.parent_full_path': 'Ganzer Pfad des Übergeordneten Elements',
'storelocation.comment': 'Kommentar',
'storelocation.comment_t': 'Kommentar (Text)',
'storelocation.last_modified': 'Änderungsdatum',
'storelocation.creation_date': 'Erstellungsdatum',
} );

View file

@ -1,69 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
CKEDITOR.plugins.setLang( 'partdb_label', 'en', {
title: 'Insert Placeholders',
label: 'Placeholders',
'section.global': 'Globals',
'section.part': 'Part',
'section.part_lot': 'Part lot',
'section.storelocation': 'Storage location',
'part.id': 'Database ID',
'part.name': 'Part name',
'part.category': 'Category',
'part.category_full': 'Category (Full path)',
'part.manufacturer': 'Manufacturer',
'part.manufacturer_full': 'Manufacturer (Full path)',
'part.footprint': 'Footprint',
'part.footprint_full': 'Footprint (Full path)',
'part.mass': 'Mass',
'part.tags': 'Tags',
'part.mpn': 'Manufacturer Product Number (MPN)',
'part.status': 'Manufacturing status',
'part.description': 'Description',
'part.description_t': 'Description (Text)',
'part.comment': 'Comment',
'part.comment_t': 'Comment (Text)',
'part.last_modified': 'Last modified datetime',
'part.creation_date': 'Creation datetime',
'global.username': 'Username',
'global.username_full': 'Username (including name)',
'global.datetime': 'Current datetime',
'global.date': 'Current date',
'global.time': 'Current time',
'global.install_name': 'Instance name',
'global.type': 'Target type',
'lot.id': 'Lot ID',
'lot.name': 'Lot name',
'lot.comment': 'Lot comment',
'lot.expiration_date': 'Expiration date',
'lot.amount': 'Lot amount',
'lot.location': 'Storage location',
'lot.location_full': 'Storage location (Full path)',
'storelocation.id': 'Location ID',
'storelocation.name': 'Name',
'storelocation.full_path': 'Full path',
'storelocation.parent_name': 'Parent name',
'storelocation.parent_full_path': 'Parent full path',
'storelocation.comment': 'Comment',
'storelocation.comment_t': 'Comment (Text)',
'storelocation.last_modified': 'Last modified datetime',
'storelocation.creation_date': 'Createion datetime',
} );

View file

@ -1,209 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Placeholder logic inspired by CKEDITOR placeholder plugin (https://github.com/ckeditor/ckeditor4/blob/master/plugins/placeholder/plugin.js)
*/
const PLACEHOLDERS = {
part: {
label: 'section.part',
entries: [
['[[ID]]', 'part.id'],
['[[NAME]]', 'part.name'],
['[[CATEGORY]]', 'part.category'],
['[[CATEGORY_FULL]]', 'part.category_full'],
['[[MANUFACTURER]]', 'part.manufacturer'],
['[[MANUFACTURER_FULL]]', 'part.manufacturer_full'],
['[[FOOTPRINT]]', 'part.footprint'],
['[[FOOTPRINT_FULL]]', 'part.footprint'],
['[[MASS]]', 'part.mass'],
['[[MPN]]', 'part.mpn'],
['[[TAGS]]', 'part.tags'],
['[[M_STATUS]]', 'part.status'],
['[[DESCRIPTION]]', 'part.description'],
['[[DESCRIPTION_T]]', 'part.description_t'],
['[[COMMENT]]', 'part.comment'],
['[[COMMENT_T]]', 'part.comment_t'],
['[[LAST_MODIFIED]]', 'part.last_modified'],
['[[CREATION_DATE]]', 'part.creation_date'],
]
},
part_lot: {
label: 'section.part_lot',
entries: [
['[[LOT_ID]]', 'lot.id'],
['[[LOT_NAME]]', 'lot.name'],
['[[LOT_COMMENT]]', 'lot.comment'],
['[[EXPIRATION_DATE]]', 'lot.expiration_date'],
['[[AMOUNT]]', 'lot.amount'],
['[[LOCATION]]', 'lot.location'],
['[[LOCATION_FULL]]', 'lot.location_full'],
]
},
storelocation: {
label: 'section.storelocation',
entries: [
['[[ID]]', 'storelocation.id'],
['[[NAME]]', 'storelocation.name'],
['[[FULL_PATH]]', 'storelocation.full_path'],
['[[PARENT]]', 'storelocation.parent_name'],
['[[PARENT_FULL_PATH]]', 'storelocation.parent_full_path'],
['[[COMMENT]]', 'storelocation.comment'],
['[[COMMENT_T]]', 'storelocation.comment_t'],
['[[LAST_MODIFIED]]', 'storelocation.last_modified'],
['[[CREATION_DATE]]', 'storelocation.creation_date'],
]
},
global: {
label: 'section.global',
entries: [
['[[USERNAME]]', 'global.username'],
['[[USERNAME_FULL]]', 'global.username_full'],
['[[DATETIME]]', 'global.datetime'],
['[[DATE]]', 'global.date'],
['[[TIME]]', 'global.time'],
['[[INSTALL_NAME]]', 'global.install_name'],
['[[TYPE]]', 'global.type']
],
},
};
function findLabelForPlaceholder(search)
{
for (var group in PLACEHOLDERS) {
var entries = PLACEHOLDERS[group].entries;
for (var placeholder in entries) {
if (entries[placeholder][0] == search) {
return entries[placeholder][1];
}
}
}
}
//Dont escape text inside of twig blocks
CKEDITOR.config.protectedSource.push(/\{\{[\s\S]*?\}\}/g);
CKEDITOR.config.protectedSource.push(/\{\%[\s\S]*?%\}/g);
CKEDITOR.plugins.add('partdb_label', {
hidpi: true,
icons: 'placeholder',
lang: ['en', 'de'],
init: function (editor) {
var config = editor.config,
lang = editor.lang.partdb_label;
var pluginDirectory = this.path;
editor.addContentsCss( pluginDirectory + 'styles/style.css' );
// Put ur init code here.
editor.widgets.add( 'placeholder', {
// Widget code.
pathName: lang.label,
// We need to have wrapping element, otherwise there are issues in
// add dialog.
template: '<span class="cke_placeholder">[[]]</span>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '[[' + this.data.name + ']]' );
},
init: function() {
// Note that placeholder markup characters are stripped for the name.
this.setData( 'name', this.element.getText().slice( 2, -2 ) );
},
data: function() {
this.element.setText( '[[' + this.data.name + ']]' );
var title = findLabelForPlaceholder( '[[' + this.data.name + ']]');
if (lang[title]) {
title = lang[title];
}
this.element.setAttribute('title', title);
},
getLabel: function() {
return this.editor.lang.widget.label.replace( /%1/, this.data.name + ' ' + this.pathName );
}
} );
editor.ui.addRichCombo('Placeholders', {
label: lang.label,
title: lang.title,
allowedContent: 'abbr[title]',
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: false,
attributes: { 'aria-label': lang.title }
},
init: function () {
for (var group in PLACEHOLDERS) {
var localized_group = PLACEHOLDERS[group].label;
if (lang[localized_group]) {
localized_group = lang[localized_group];
}
this.startGroup(localized_group);
var entries = PLACEHOLDERS[group].entries;
for (var placeholder in entries) {
var localized_placeholder = entries[placeholder][1];
if (lang[localized_placeholder]) {
localized_placeholder = lang[localized_placeholder];
}
this.add(entries[placeholder][0], localized_placeholder, entries[placeholder][0])
}
}
},
onClick: function(value) {
editor.focus();
editor.fire('saveSnapshot');
editor.insertText(value);
}
});
},
afterInit: function( editor ) {
var placeholderReplaceRegex = /\[\[([^\[\]])+\]\]/g;
editor.dataProcessor.dataFilter.addRules({
text: function (text, node) {
var dtd = node.parent && CKEDITOR.dtd[node.parent.name];
// Skip the case when placeholder is in elements like <title> or <textarea>
// but upcast placeholder in custom elements (no DTD).
if (dtd && !dtd.span)
return;
return text.replace(placeholderReplaceRegex, function (match) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element('span', {
'class': 'cke_placeholder'
});
// Adds placeholder identifier as innertext.
innerElement.add(new CKEDITOR.htmlParser.text(match));
widgetWrapper = editor.widgets.wrapElement(innerElement, 'placeholder');
// Return outerhtml of widget wrapper so it will be placed
// as replacement.
return widgetWrapper.getOuterHtml();
});
}
});
}
});

View file

@ -1,117 +0,0 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@font-face {
font-family: "DejaVu Sans Mono";
font-style: normal;
font-weight: normal;
src: local(DejaVu Sans Mono), local(DejaVuSansMono),
url(DejaVuSansMono.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: normal;
font-weight: bold;
src: local(DejaVu Sans Mono Bold), local(DejaVuSansMono-Bold),
url(DejaVuSansMono-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: oblique;
font-weight: bold;
src: local(DejaVu Sans Mono Bold Oblique), local(DejaVuSansMono-BoldOblique),
url(DejaVuSansMono-BoldOblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: oblique;
font-weight: normal;
src: local(DejaVu Sans Mono Oblique), local(DejaVuSansMono-Oblique),
url(DejaVuSansMono-Oblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: normal;
font-weight: normal;
src: local(DejaVu Sans), local(DejaVuSans),
url(DejaVuSans.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: normal;
font-weight: bold;
src: local(DejaVu Sans Bold), local(DejaVuSans-Bold),
url(DejaVuSans-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: oblique;
font-weight: bold;
src: local(DejaVu Sans Bold Oblique), local(DejaVuSans-BoldOblique),
url(DejaVuSans-BoldOblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: oblique;
font-weight: normal;
src: local(DejaVu Sans Oblique), local(DejaVuSans-Oblique),
url(DejaVuSans-Oblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: normal;
font-weight: normal;
src: local(DejaVu Serif), local(DejaVuSerif),
url(DejaVuSerif.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: normal;
font-weight: bold;
src: local(DejaVu Serif Bold), local(DejaVuSerif-Bold),
url(DejaVuSerif-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: italic;
font-weight: bold;
src: local(DejaVu Serif Bold Italic), local(DejaVuSerif-BoldItalic),
url(DejaVuSerif-BoldItalic.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: italic;
font-weight: normal;
src: local(DejaVu Serif Italic), local(DejaVuSerif-Italic),
url(DejaVuSerif-Italic.woff) format("woff");
}
.cke_placeholder {
background-color:#ff0
}
.cke_editable {
font-family: "DejaVu Sans Mono";
font-size: 9pt;
line-height: 1.5;
}

View file

@ -1,52 +0,0 @@
CKEDITOR.dialog.add( 'showProtectedDialog', function( editor ) {
return {
title: 'Edit Protected Source',
minWidth: 300,
minHeight: 60,
onOk: function() {
var newSourceValue = this.getContentElement( 'info', 'txtProtectedSource' ).getValue();
var encodedSourceValue = CKEDITOR.plugins.showprotected.encodeProtectedSource( newSourceValue );
this._.selectedElement.setAttribute('data-cke-realelement', encodedSourceValue);
this._.selectedElement.setAttribute('title', newSourceValue);
this._.selectedElement.setAttribute('alt', newSourceValue);
},
onHide: function() {
delete this._.selectedElement;
},
onShow: function() {
this._.selectedElement = editor.getSelection().getSelectedElement();
var decodedSourceValue = CKEDITOR.plugins.showprotected.decodeProtectedSource( this._.selectedElement.getAttribute('data-cke-realelement') );
this.setValueOf( 'info', 'txtProtectedSource', decodedSourceValue );
},
contents: [
{
id: 'info',
label: 'Edit Protected Source',
accessKey: 'I',
elements: [
{
type: 'text',
id: 'txtProtectedSource',
label: 'Value',
required: true,
validate: function() {
if ( !this.getValue() ) {
alert( 'The value cannot be empty' );
return false;
}
return true;
}
}
]
}
]
};
} );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

View file

@ -1,105 +0,0 @@
/*
* "showprotected" CKEditor plugin
*
* Created by Matthew Lieder (https://github.com/IGx89)
*
* Licensed under the MIT, GPL, LGPL and MPL licenses
*
* Icon courtesy of famfamfam: http://www.famfamfam.com/lab/icons/mini/
*/
// TODO: configuration settings
// TODO: show the actual text inline, not just an icon?
// TODO: improve copy/paste behavior (tooltip is wrong after paste)
CKEDITOR.plugins.add( 'showprotected', {
requires: 'dialog,fakeobjects',
onLoad: function() {
// Add the CSS styles for protected source placeholders.
var iconPath = CKEDITOR.getUrl( this.path + 'images' + '/code.gif' ),
baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
var template = '.%2 img.cke_protected' +
'{' +
baseStyle +
'width:16px;' +
'min-height:15px;' +
// The default line-height on IE.
'height:1.15em;' +
// Opera works better with "middle" (even if not perfect)
'vertical-align:' + ( CKEDITOR.env.opera ? 'middle' : 'text-bottom' ) + ';' +
'}';
// Styles with contents direction awareness.
function cssWithDir( dir ) {
return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
}
CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
},
init: function( editor ) {
CKEDITOR.dialog.add( 'showProtectedDialog', this.path + 'dialogs/protected.js' );
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'img' ) && element.hasClass( 'cke_protected' ) ) {
evt.data.dialog = 'showProtectedDialog';
}
} );
},
afterInit: function( editor ) {
// Register a filter to displaying placeholders after mode change.
var dataProcessor = editor.dataProcessor,
dataFilter = dataProcessor && dataProcessor.dataFilter;
if ( dataFilter ) {
dataFilter.addRules( {
comment: function( commentText, commentElement ) {
if(commentText.indexOf(CKEDITOR.plugins.showprotected.protectedSourceMarker) == 0) {
commentElement.attributes = [];
var fakeElement = editor.createFakeParserElement( commentElement, 'cke_protected', 'protected' );
var cleanedCommentText = CKEDITOR.plugins.showprotected.decodeProtectedSource( commentText );
fakeElement.attributes.title = fakeElement.attributes.alt = cleanedCommentText;
return fakeElement;
}
return null;
}
} );
}
}
} );
/**
* Set of showprotected plugin's helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.showprotected = {
protectedSourceMarker: '{cke_protected}',
decodeProtectedSource: function( protectedSource ) {
if(protectedSource.indexOf('%3C!--') == 0) {
return decodeURIComponent(protectedSource).replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) {
return decodeURIComponent( data );
} );
} else {
return decodeURIComponent(protectedSource.substr(CKEDITOR.plugins.showprotected.protectedSourceMarker.length));
}
},
encodeProtectedSource: function( protectedSource ) {
return '<!--' + CKEDITOR.plugins.showprotected.protectedSourceMarker +
encodeURIComponent( protectedSource ).replace( /--/g, '%2D%2D' ) +
'-->';
}
};

View file

@ -263,9 +263,9 @@ abstract class BaseAdminController extends AbstractController
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->entityManager->getRepository($this->entity_class);
return $this->render($this->twig_template, [
return $this->renderForm($this->twig_template, [
'entity' => $entity,
'form' => $form->createView(),
'form' => $form,
'route_base' => $this->route_base,
'datatable' => $table,
'pdf_data' => $pdf_data ?? null,
@ -397,11 +397,11 @@ abstract class BaseAdminController extends AbstractController
$em->flush();
}
return $this->render($this->twig_template, [
return $this->renderForm($this->twig_template, [
'entity' => $new_entity,
'form' => $form->createView(),
'import_form' => $import_form->createView(),
'mass_creation_form' => $mass_creation_form->createView(),
'form' => $form,
'import_form' => $import_form,
'mass_creation_form' => $mass_creation_form,
'route_base' => $this->route_base,
]);
}

View file

@ -127,8 +127,8 @@ class LabelController extends AbstractController
}
}
return $this->render('LabelSystem/dialog.html.twig', [
'form' => $form->createView(),
return $this->renderForm('LabelSystem/dialog.html.twig', [
'form' => $form,
'pdf_data' => $pdf_data,
'filename' => $filename,
'profile' => $profile,

View file

@ -189,11 +189,11 @@ class PartController extends AbstractController
$this->addFlash('error', 'part.edited_flash.invalid');
}
return $this->render('Parts/edit/edit_part_info.html.twig',
[
'part' => $part,
'form' => $form->createView(),
]);
return $this->renderForm('Parts/edit/edit_part_info.html.twig',
[
'part' => $part,
'form' => $form,
]);
}
/**
@ -313,10 +313,10 @@ class PartController extends AbstractController
$this->addFlash('error', 'part.created_flash.invalid');
}
return $this->render('Parts/edit/new_part.html.twig',
[
'part' => $new_part,
'form' => $form->createView(),
]);
return $this->renderForm('Parts/edit/new_part.html.twig',
[
'part' => $new_part,
'form' => $form,
]);
}
}

View file

@ -203,12 +203,13 @@ class PartListsController extends AbstractController
}
/**
* @Route("/parts/by_tag/{tag}", name="part_list_tags")
* @Route("/parts/by_tag/{tag}", name="part_list_tags", requirements={"tag": ".*"})
*
* @return JsonResponse|Response
*/
public function showTag(string $tag, Request $request, DataTableFactory $dataTable)
{
$tag = trim($tag);
$table = $dataTable->createFromType(PartsDataTable::class, ['tag' => $tag])
->handleRequest($request);

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