Merge branch 'module/spreadsheet' into dev
This commit is contained in:
@@ -37,12 +37,51 @@ import org.json.JSONObject;
|
|||||||
public class Util {
|
public class Util {
|
||||||
public static final System.Logger LOG = System.getLogger("Util");
|
public static final System.Logger LOG = System.getLogger("Util");
|
||||||
private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
|
private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL);
|
||||||
|
private static final Pattern SPREADSHEET_PATTERN = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL);
|
||||||
private static File plantumlJar = null;
|
private static File plantumlJar = null;
|
||||||
private static final JParsedown MARKDOWN = new JParsedown();
|
private static final JParsedown MARKDOWN = new JParsedown();
|
||||||
public static final String SHA1 = "SHA-1";
|
public static final String SHA1 = "SHA-1";
|
||||||
private static final MessageDigest SHA1_DIGEST;
|
private static final MessageDigest SHA1_DIGEST;
|
||||||
private static final Map<Integer,String> umlCache = new HashMap<>();
|
private static final Map<Integer,String> umlCache = new HashMap<>();
|
||||||
|
|
||||||
|
private static final String SCRIPT = """
|
||||||
|
<script src="http://127.0.0.1:8080/js/jspreadsheet-ce.js"></script>
|
||||||
|
<div id="spreadsheet"></div>
|
||||||
|
<script type="application/javascript">
|
||||||
|
alert('Test');
|
||||||
|
jspreadsheet(document.getElementById('spreadsheet'), {
|
||||||
|
worksheets: [
|
||||||
|
{
|
||||||
|
data: [
|
||||||
|
['Jazz', 'Honda', '2019-02-12', '', true, '$ 2.000,00', '#777700'],
|
||||||
|
['Civic', 'Honda', '2018-07-11', '', true, '$ 4.000,01', '#007777'],
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
{ type: 'text', title: 'Car', width: 120 },
|
||||||
|
{
|
||||||
|
type: 'dropdown',
|
||||||
|
title: 'Make',
|
||||||
|
width: 200,
|
||||||
|
source: ['Alfa Romeo', 'Audi', 'Bmw', 'Honda'],
|
||||||
|
},
|
||||||
|
{ type: 'calendar', title: 'Available', width: 200 },
|
||||||
|
{ type: 'image', title: 'Photo', width: 120 },
|
||||||
|
{ type: 'checkbox', title: 'Stock', width: 80 },
|
||||||
|
{
|
||||||
|
type: 'numeric',
|
||||||
|
title: 'Price',
|
||||||
|
width: 100,
|
||||||
|
mask: '$ #.##,00',
|
||||||
|
decimal: ',',
|
||||||
|
},
|
||||||
|
{ type: 'color', width: 100, render: 'square' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
""";
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
SHA1_DIGEST = MessageDigest.getInstance(SHA1);
|
SHA1_DIGEST = MessageDigest.getInstance(SHA1);
|
||||||
@@ -79,8 +118,22 @@ public class Util {
|
|||||||
public static String markdown(String source){
|
public static String markdown(String source){
|
||||||
if (source == null) return source;
|
if (source == null) return source;
|
||||||
try {
|
try {
|
||||||
|
var matcher = SPREADSHEET_PATTERN.matcher(source);
|
||||||
|
var count = 0;
|
||||||
|
while (matcher.find()){
|
||||||
|
count++;
|
||||||
|
var sheetData = matcher.group(0).trim();
|
||||||
|
var start = matcher.start(0);
|
||||||
|
var end = matcher.end(0);
|
||||||
|
source = source.substring(0, start)
|
||||||
|
+ "<div class=\"spreadsheet\" id=\"spreadsheet-"+count+"\">"
|
||||||
|
+ sheetData.substring(11,sheetData.length()-10)
|
||||||
|
+ "</div>"
|
||||||
|
+ source.substring(end);
|
||||||
|
matcher = SPREADSHEET_PATTERN.matcher(source);
|
||||||
|
}
|
||||||
if (plantumlJar != null && plantumlJar.exists()) {
|
if (plantumlJar != null && plantumlJar.exists()) {
|
||||||
var matcher = UML_PATTERN.matcher(source);
|
matcher = UML_PATTERN.matcher(source);
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
var uml = matcher.group(0).trim();
|
var uml = matcher.group(0).trim();
|
||||||
var start = matcher.start(0);
|
var start = matcher.start(0);
|
||||||
|
|||||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspreadsheet-ce": "^5.0.4",
|
||||||
"svelte-tiny-router": "^1.0.5"
|
"svelte-tiny-router": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -488,6 +489,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jspreadsheet/formula": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jspreadsheet/formula/-/formula-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-PDQYf9REQA53I7tVYkvkeyQxrd5jcjUeHgItYnRpjN2QiIQwawSqBDtGGEVQTSboTG+JwgGCuhvOpj7FxeKwew=="
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.44.1",
|
"version": "4.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
|
||||||
@@ -951,6 +957,20 @@
|
|||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspreadsheet-ce": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspreadsheet-ce/-/jspreadsheet-ce-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-ra1JI1n+tEGgRMzTzNkPZjG0HZz8W6bFGAiTiHl+eYarXdRmS5qDc/ua3l2ev7oZ6Og9kjfrXYHVLUWiVc308w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jspreadsheet/formula": "^2.0.2",
|
||||||
|
"jsuites": "^5.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsuites": {
|
||||||
|
"version": "5.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsuites/-/jsuites-5.13.5.tgz",
|
||||||
|
"integrity": "sha512-cvkcpy/v5I3+IAcNPE4UP38PFCEfUQw9JI5NN61dlcXLwkD+2UTIOsRPvgMLeqI1eDWHL4AHfrbcE/+TFciUsw=="
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jspreadsheet-ce": "^5.0.4",
|
||||||
"svelte-tiny-router": "^1.0.5"
|
"svelte-tiny-router": "^1.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
import ResetPw from "./routes/user/ResetPw.svelte";
|
import ResetPw from "./routes/user/ResetPw.svelte";
|
||||||
import Search from "./routes/search/Search.svelte";
|
import Search from "./routes/search/Search.svelte";
|
||||||
import SendDoc from "./routes/document/Send.svelte";
|
import SendDoc from "./routes/document/Send.svelte";
|
||||||
|
import Spreadsheet from "./routes/calc.svelte";
|
||||||
import Stock from './routes/stock/Index.svelte';
|
import Stock from './routes/stock/Index.svelte';
|
||||||
import TagList from "./routes/tags/Index.svelte";
|
import TagList from "./routes/tags/Index.svelte";
|
||||||
import TagUses from "./routes/tags/TagUses.svelte";
|
import TagUses from "./routes/tags/TagUses.svelte";
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
<Route path="/" component={User} />
|
<Route path="/" component={User} />
|
||||||
<Route path="/bookmark" component={Bookmarks} />
|
<Route path="/bookmark" component={Bookmarks} />
|
||||||
<Route path="/bookmark/:id/view" component={Bookmark} />
|
<Route path="/bookmark/:id/view" component={Bookmark} />
|
||||||
|
<Route path="/calc" component={Spreadsheet} />
|
||||||
<Route path="/company" component={Companies} />
|
<Route path="/company" component={Companies} />
|
||||||
<Route path="/contact" component={ContactList} />
|
<Route path="/contact" component={ContactList} />
|
||||||
<Route path="/document" component={DocList} />
|
<Route path="/document" component={DocList} />
|
||||||
|
|||||||
78
frontend/src/Components/MarkdownDisplay.svelte
Normal file
78
frontend/src/Components/MarkdownDisplay.svelte
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
let { classes='markdown', markdown=$bindable({source:'',rendered:''}), onclick = null, oncontextmenu = null, title='', wrapper = 'div' } = $props();
|
||||||
|
let jspreadsheet = null;
|
||||||
|
const regex = /@startsheet[\s\S]*?@endsheet/g;
|
||||||
|
const number = /^[0-9.-]+$/
|
||||||
|
|
||||||
|
function update(sheet, index){
|
||||||
|
const data = sheet.getData(false,false,'|',false);
|
||||||
|
markdown.source = replaceNthSpreadsheet(markdown.source,index,data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceNthSpreadsheet(text, n, newContent) {
|
||||||
|
const blocks = text.match(regex) || [];
|
||||||
|
if (blocks.length < n+1){
|
||||||
|
console.warn(`cannot replace block ${n}: only ${blocks.length} blocks found!`);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
let count = 0;
|
||||||
|
return text.replace(regex, (match) => count++ === n ? `@startsheet\n${newContent}\n@endsheet` : match);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCell(cell, value, x, y, instance, options){
|
||||||
|
value = value.trim();
|
||||||
|
if (value.startsWith('=') || number.test(value)) cell.style.textAlign = 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transform(){
|
||||||
|
if (!markdown.rendered) return;
|
||||||
|
let sheets = document.getElementsByClassName('spreadsheet');
|
||||||
|
for (let i = 0; i < sheets.length; i++) {
|
||||||
|
if (!jspreadsheet) {
|
||||||
|
let module = await import('jspreadsheet-ce'); // path or package name
|
||||||
|
await import('jspreadsheet-ce/dist/jspreadsheet.css');
|
||||||
|
jspreadsheet = module.default ?? module;
|
||||||
|
}
|
||||||
|
if (!jspreadsheet) break; // break loop if library fails to load
|
||||||
|
|
||||||
|
let sheet = sheets[i];
|
||||||
|
let raw = sheet.innerHTML.trim();
|
||||||
|
|
||||||
|
// Use parseCSV from the helpers
|
||||||
|
const parsed = jspreadsheet.helpers.parseCSV(raw, '|');
|
||||||
|
let columns = {};
|
||||||
|
|
||||||
|
for (let row of parsed){
|
||||||
|
for (let col in row){
|
||||||
|
let data = ""+row[col];
|
||||||
|
if (data.startsWith('=')) continue;
|
||||||
|
let len = data.length;
|
||||||
|
columns[col] = Math.max(columns[col]??0,len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
columns = Object.values(columns).map((len) => {return {
|
||||||
|
align: 'left',
|
||||||
|
render: formatCell,
|
||||||
|
width:`${len}0px`
|
||||||
|
}});
|
||||||
|
let config = {
|
||||||
|
worksheets : [{
|
||||||
|
data:parsed,
|
||||||
|
columns
|
||||||
|
}],
|
||||||
|
onchange : (instance, cell, x, y, value) => update(instance, i)
|
||||||
|
};
|
||||||
|
let wb = jspreadsheet(document.getElementById(sheet.id), config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => { setTimeout(transform,200)});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if markdown.rendered}
|
||||||
|
<svelte:element this={wrapper} class={classes} {onclick} {oncontextmenu} {title}>
|
||||||
|
{@html markdown.rendered}
|
||||||
|
</svelte:element>
|
||||||
|
{/if}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import { api, target } from '../urls.svelte.js';
|
import { api, target } from '../urls.svelte.js';
|
||||||
import { t } from '../translations.svelte.js';
|
import { t } from '../translations.svelte.js';
|
||||||
|
|
||||||
|
import Display from './MarkdownDisplay.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
editable = true,
|
editable = true,
|
||||||
onclick = evt => {},
|
onclick = evt => {},
|
||||||
@@ -136,7 +138,7 @@
|
|||||||
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
|
<span id="restore_markdown" onclick={restore} class="hint">{t('unsaved_content')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea>
|
<textarea bind:value={editValue.source} onkeyup={typed} autofocus={!simple}></textarea>
|
||||||
<div class="preview">{@html target(editValue.rendered)}</div>
|
<Display classes="preview" bind:markdown={editValue} />
|
||||||
{#if !simple}
|
{#if !simple}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
|
<button class="cancel" onclick={e => editing = false}>{t('cancel')}</button>
|
||||||
@@ -144,6 +146,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:element this={type} {onclick} {oncontextmenu} class={{editable}} title={t('right_click_to_edit')} >{@html target(value.rendered)}</svelte:element>
|
<Display classes={{editable}} markdown={value} {onclick} {oncontextmenu} title={t('right_click_to_edit')} wrapper={type} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
59
frontend/src/routes/calc.svelte
Normal file
59
frontend/src/routes/calc.svelte
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
var spreadsheet = null;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
worksheets: [{
|
||||||
|
data: [
|
||||||
|
["1","Sum of A:","=SUM(A1:A99)"],
|
||||||
|
["2"],
|
||||||
|
["3"],
|
||||||
|
["4"]
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
{ type: 'autonumber', title: 'amount' },
|
||||||
|
{ type: 'text', width: '350px', title: 'description', align: 'right' },
|
||||||
|
{ type: 'text', width: '250px', title: 'value' },
|
||||||
|
],
|
||||||
|
// Name of the worksheet
|
||||||
|
worksheetName: 'Albums'
|
||||||
|
}],
|
||||||
|
onchange: update
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function update(instance, cell, x, y, value) {
|
||||||
|
console.log({instance,cell,x,y,value});
|
||||||
|
console.log(spreadsheet[0].getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = true;
|
||||||
|
let module = null;
|
||||||
|
|
||||||
|
async function load(){
|
||||||
|
try {
|
||||||
|
const module = await import('jspreadsheet-ce'); // path or package name
|
||||||
|
await import('jspreadsheet-ce/dist/jspreadsheet.css');
|
||||||
|
|
||||||
|
let jspreadsheet = module.default ?? module;
|
||||||
|
let element = document.getElementById('spreadsheet');
|
||||||
|
console.log(element);
|
||||||
|
spreadsheet = jspreadsheet(element, config);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
Loading…
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
<div id="spreadsheet">Spreadsheet loading…</div>
|
||||||
@@ -11,3 +11,20 @@ tasks.processResources {
|
|||||||
}
|
}
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun download(url : String, destination : String){
|
||||||
|
var destFile = projectDir.toPath().resolve(destination).toFile();
|
||||||
|
destFile.parentFile.mkdirs()
|
||||||
|
if (!destFile.exists()) {
|
||||||
|
System.out.println("Downloading "+url)
|
||||||
|
ant.invokeMethod("get", mapOf("src" to url, "dest" to destFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("downloadLib"){
|
||||||
|
download("https://bossanova.uk/jspreadsheet/v5/jspreadsheet.js", "src/main/resources/web/js/jspreadsheet-ce.js")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("compileJava") {
|
||||||
|
dependsOn("downloadLib")
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,6 +317,23 @@ tr:hover .taglist .tag button {
|
|||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jss_worksheet{
|
||||||
|
background: black !important;
|
||||||
|
border-right: 1px solid #333 !important;
|
||||||
|
border-bottom: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jss_worksheet > thead > tr > td,
|
||||||
|
.jss_worksheet > tbody > tr > td:first-child{
|
||||||
|
background: #730000 !important;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jss_worksheet td{
|
||||||
|
border-top: 1px solid #333 !important;
|
||||||
|
border-left: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
background: black;
|
||||||
|
|||||||
@@ -308,6 +308,23 @@ tr:hover .taglist .tag button {
|
|||||||
background: #a00;
|
background: #a00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jss_worksheet{
|
||||||
|
background: black !important;
|
||||||
|
border-right: 1px solid #333 !important;
|
||||||
|
border-bottom: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jss_worksheet > thead > tr > td,
|
||||||
|
.jss_worksheet > tbody > tr > td:first-child{
|
||||||
|
background: orange !important;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jss_worksheet td{
|
||||||
|
border-top: 1px solid #333 !important;
|
||||||
|
border-left: 1px solid #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#app nav a{
|
#app nav a{
|
||||||
background: black;
|
background: black;
|
||||||
|
|||||||
8
web/src/main/resources/web/js/jspreadsheet-ce.js
Normal file
8
web/src/main/resources/web/js/jspreadsheet-ce.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user