├── entry ├── .gitignore ├── package.json ├── src │ ├── main │ │ ├── js │ │ │ └── default │ │ │ │ ├── pages │ │ │ │ └── index │ │ │ │ │ ├── index.css │ │ │ │ │ ├── index.hml │ │ │ │ │ └── index.js │ │ │ │ ├── i18n │ │ │ │ ├── zh-CN.json │ │ │ │ └── en-US.json │ │ │ │ ├── common │ │ │ │ └── images │ │ │ │ │ ├── bg-tv.jpg │ │ │ │ │ └── Wallpaper.png │ │ │ │ ├── app.js │ │ │ │ └── components │ │ │ │ ├── index.hml │ │ │ │ ├── index.css │ │ │ │ ├── table │ │ │ │ ├── alphabet.js │ │ │ │ ├── canvas2d.js │ │ │ │ ├── cell-render.js │ │ │ │ ├── range.js │ │ │ │ ├── index.js │ │ │ │ ├── render.js │ │ │ │ ├── viewport.js │ │ │ │ └── area.js │ │ │ │ └── index.js │ │ ├── resources │ │ │ └── base │ │ │ │ ├── media │ │ │ │ └── icon.png │ │ │ │ └── element │ │ │ │ └── string.json │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── sheet │ │ │ │ ├── MyApplication.java │ │ │ │ └── MainAbility.java │ │ └── config.json │ └── ohosTest │ │ ├── js │ │ ├── test │ │ │ ├── List.test.js │ │ │ └── ExampleJsunit.test.js │ │ └── default │ │ │ ├── i18n │ │ │ ├── zh-CN.json │ │ │ └── en-US.json │ │ │ ├── pages │ │ │ └── index │ │ │ │ ├── index.hml │ │ │ │ ├── index.css │ │ │ │ └── index.js │ │ │ └── app.js │ │ └── resources │ │ └── base │ │ ├── media │ │ └── icon.png │ │ └── element │ │ └── string.json ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── previewer │ ├── previewConfig.json │ └── phone │ │ └── phoneSettingConfig_-172715701.json ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── screenshots ├── 1.gif ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.gif ├── 8.png ├── 9.gif └── 10.png ├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── gradlew └── README.md /entry/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /entry/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':entry' 2 | -------------------------------------------------------------------------------- /entry/src/main/js/default/pages/index/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entry/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # config module specific ProGuard rules here. -------------------------------------------------------------------------------- /entry/src/ohosTest/js/test/List.test.js: -------------------------------------------------------------------------------- 1 | require('./ExampleJsunit.test.js') -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /screenshots/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/1.gif -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/7.gif -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/8.png -------------------------------------------------------------------------------- /screenshots/9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/9.gif -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/screenshots/10.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /entry/src/main/js/default/i18n/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "strings": { 3 | "hello": "您好", 4 | "world": "世界" 5 | } 6 | } -------------------------------------------------------------------------------- /entry/src/main/js/default/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "strings": { 3 | "hello": "Hello", 4 | "world": "World" 5 | } 6 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/entry/src/main/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/main/js/default/common/images/bg-tv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/entry/src/main/js/default/common/images/bg-tv.jpg -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/entry/src/ohosTest/resources/base/media/icon.png -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/i18n/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "strings": { 3 | "hello": "您好", 4 | "world": "世界" 5 | }, 6 | "Files": { 7 | } 8 | } -------------------------------------------------------------------------------- /entry/src/main/js/default/common/images/Wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/sheet/main/entry/src/main/js/default/common/images/Wallpaper.png -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "strings": { 3 | "hello": "Hello", 4 | "world": "World" 5 | }, 6 | "Files": { 7 | } 8 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/pages/index/index.hml: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ $t('strings.hello') }} {{title}} 4 | 5 |
6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/previewer/previewConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": { 3 | "LastPreviewDevice": { 4 | "/Users/eno/DevEcoStudioProjects/sheet/entry/.preview/intermediates/res/debug": [ 5 | "phone" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/pages/index/index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | flex-direction: column; 3 | justify-content: center; 4 | align-items: center; 5 | } 6 | 7 | .title { 8 | font-size: 100px; 9 | } 10 | -------------------------------------------------------------------------------- /entry/src/main/js/default/app.js: -------------------------------------------------------------------------------- 1 | export default { 2 | onCreate() { 3 | console.info('AceApplication onCreate'); 4 | }, 5 | onDestroy() { 6 | console.info('AceApplication onDestroy'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/app.js: -------------------------------------------------------------------------------- 1 | export default { 2 | onCreate() { 3 | console.info('TestApplication onCreate'); 4 | }, 5 | onDestroy() { 6 | console.info('TestApplication onDestroy'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://repo.huaweicloud.com/gradle/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string":[ 3 | { 4 | "name":"entry_MainAbility", 5 | "value":"entry_MainAbility" 6 | }, 7 | { 8 | "name":"mainability_description", 9 | "value":"JS_Empty Ability" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | /entry/.preview 15 | .cxx 16 | -------------------------------------------------------------------------------- /entry/src/main/java/com/example/sheet/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.sheet; 2 | 3 | import ohos.aafwk.ability.AbilityPackage; 4 | 5 | public class MyApplication extends AbilityPackage { 6 | @Override 7 | public void onInitialize(){ 8 | super.onInitialize(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /entry/src/main/js/default/pages/index/index.hml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /entry/src/ohosTest/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string":[ 3 | { 4 | "name":"app_name", 5 | "value":"sheet" 6 | }, 7 | { 8 | "name": "mainability_description", 9 | "value": "hap sample empty page" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /entry/src/ohosTest/js/test/ExampleJsunit.test.js: -------------------------------------------------------------------------------- 1 | import app from '@system.app' 2 | import {describe, beforeAll, beforeEach, afterEach, afterAll, it, expect} from 'deccjsunit/index' 3 | describe('appInfoTest', function () { 4 | it('app_info_test_001', 0, function () { 5 | var info = app.getInfo() 6 | expect(info.versionName).assertEqual('1.0') 7 | expect(info.versionCode).assertEqual('3') 8 | }) 9 | }) -------------------------------------------------------------------------------- /entry/src/main/java/com/example/sheet/MainAbility.java: -------------------------------------------------------------------------------- 1 | package com.example.sheet; 2 | 3 | import ohos.ace.ability.AceAbility; 4 | import ohos.aafwk.content.Intent; 5 | 6 | public class MainAbility extends AceAbility { 7 | @Override 8 | public void onStart(Intent intent) { 9 | super.onStart(intent); 10 | } 11 | 12 | @Override 13 | public void onStop() { 14 | super.onStop(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.idea/previewer/phone/phoneSettingConfig_-172715701.json: -------------------------------------------------------------------------------- 1 | { 2 | "setting": { 3 | "1.0.1": { 4 | "Language": { 5 | "args": { 6 | "Language": "zh-CN" 7 | } 8 | } 9 | } 10 | }, 11 | "frontend": { 12 | "1.0.0": { 13 | "Resolution": { 14 | "args": { 15 | "Resolution": "360*780" 16 | } 17 | }, 18 | "DeviceType": { 19 | "args": { 20 | "DeviceType": "phone" 21 | } 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /entry/src/main/js/default/components/index.hml: -------------------------------------------------------------------------------- 1 |
2 | 4 |
6 | 8 |
9 |
-------------------------------------------------------------------------------- /entry/src/main/js/default/components/index.css: -------------------------------------------------------------------------------- 1 | .area { 2 | position: absolute; 3 | text-align: left; 4 | border: 2px solid #4b89ff; 5 | line-height: 0; 6 | z-index: 100; 7 | background-color: #ffffff; 8 | pointer-events: auto; 9 | } 10 | 11 | textarea { 12 | box-sizing: content-box; 13 | padding: 2 3px; 14 | outline: none; 15 | resize: none; 16 | text-align: center; 17 | overflow-y: hidden; 18 | font: 400 13px Arial, 'Lato', 'Source Sans Pro', Roboto, Helvetica, sans-serif; 19 | white-space: normal; 20 | word-wrap: break-word; 21 | line-height: 25px; 22 | margin: 0; 23 | } 24 | 25 | .canvas-container{ 26 | position: fixed; 27 | top: 0px; 28 | left: 0px; 29 | width: 100%; 30 | height: 100%; 31 | background-color: cornflowerblue; 32 | } 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. DevEco Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | # If the Chinese output is garbled, please configure the following parameter. 10 | # This function is enabled by default when the DevEco Studio builds the hap/app,if you need disable gradle parallel,you should set org.gradle.parallel false. 11 | # more information see https://docs.gradle.org/current/userguide/performance.html 12 | # org.gradle.parallel=false 13 | # org.gradle.jvmargs=-Dfile.encoding=GBK -------------------------------------------------------------------------------- /entry/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.huawei.ohos.hap' 2 | apply plugin: 'com.huawei.ohos.decctest' 3 | //For instructions on signature configuration, see https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ide_debug_device-0000001053822404#ZH-CN_TOPIC_0000001154985555__section1112183053510 4 | ohos { 5 | compileSdkVersion 5 6 | defaultConfig { 7 | compatibleSdkVersion 5 8 | } 9 | buildTypes { 10 | release { 11 | proguardOpt { 12 | proguardEnabled false 13 | rulesFiles 'proguard-rules.pro' 14 | } 15 | } 16 | } 17 | 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.har']) 22 | testImplementation 'junit:junit:4.13' 23 | ohosTestImplementation 'com.huawei.ohos.testkit:runner:1.0.0.100' 24 | } 25 | decc { 26 | supportType = ['html','xml'] 27 | } 28 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /entry/src/main/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "bundleName":"com.example.sheet", 4 | "vendor":"example", 5 | "version":{ 6 | "code":1000000, 7 | "name":"1.0.0" 8 | } 9 | }, 10 | "deviceConfig":{}, 11 | "module":{ 12 | "package":"com.example.sheet", 13 | "name":".MyApplication", 14 | "mainAbility":"com.example.sheet.MainAbility", 15 | "deviceType":[ 16 | "phone" 17 | ], 18 | "distro":{ 19 | "deliveryWithInstall":true, 20 | "moduleName":"entry", 21 | "moduleType":"entry", 22 | "installationFree":true 23 | }, 24 | "abilities":[ 25 | { 26 | "skills":[ 27 | { 28 | "entities":[ 29 | "entity.system.home" 30 | ], 31 | "actions":[ 32 | "action.system.home" 33 | ] 34 | } 35 | ], 36 | "name":"com.example.sheet.MainAbility", 37 | "icon":"$media:icon", 38 | "description":"$string:mainability_description", 39 | "label":"$string:entry_MainAbility", 40 | "type":"page", 41 | "launchType":"standard" 42 | } 43 | ], 44 | "js":[ 45 | { 46 | "pages":[ 47 | "pages/index/index" 48 | ], 49 | "name":"default", 50 | "window":{ 51 | "designWidth":720, 52 | "autoDesignWidth":true 53 | } 54 | } 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/js/default/pages/index/index.js: -------------------------------------------------------------------------------- 1 | import file from '@system.file' 2 | import app from '@system.app' 3 | import device from '@system.device' 4 | import router from '@system.router' 5 | import {Core, ExpectExtend, ReportExtend, InstrumentLog} from 'deccjsunit/index' 6 | 7 | export default { 8 | data: { 9 | title: "" 10 | }, 11 | onInit() { 12 | this.title = this.$t('strings.world'); 13 | }, 14 | onShow() { 15 | console.info('onShow finish') 16 | const core = Core.getInstance() 17 | const expectExtend = new ExpectExtend({ 18 | 'id': 'extend' 19 | }) 20 | const reportExtend = new ReportExtend(file) 21 | const instrumentLog = new InstrumentLog({ 22 | 'id': 'report' 23 | }) 24 | core.addService('expect', expectExtend) 25 | core.addService('report', reportExtend) 26 | core.addService('report', instrumentLog) 27 | core.init() 28 | core.subscribeEvent('spec', instrumentLog) 29 | core.subscribeEvent('suite', instrumentLog) 30 | core.subscribeEvent('task', instrumentLog) 31 | 32 | const configService = core.getDefaultService('config') 33 | configService.setConfig(this) 34 | 35 | require('../../../test/List.test') 36 | core.execute() 37 | }, 38 | onReady() { 39 | }, 40 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/alphabet.js: -------------------------------------------------------------------------------- 1 | const alphabets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 2 | 3 | export function stringAt(index) { 4 | let str = ''; 5 | let cindex = index; 6 | while (cindex >= alphabets.length) { 7 | cindex /= alphabets.length; 8 | cindex -= 1; 9 | str += alphabets[parseInt(cindex, 10) % alphabets.length]; 10 | } 11 | const last = index % alphabets.length; 12 | str += alphabets[last]; 13 | return str; 14 | } 15 | 16 | export function indexAt(str) { 17 | let ret = 0; 18 | for (let i = 0; i < str.length - 1; i += 1) { 19 | const cindex = str.charCodeAt(i) - 65; 20 | const exponet = str.length - 1 - i; 21 | ret += (alphabets.length ** exponet) + (alphabets.length * cindex); 22 | } 23 | ret += str.charCodeAt(str.length - 1) - 65; 24 | return ret; 25 | } 26 | 27 | // B10 => x,y 28 | export function expr2xy(src) { 29 | let x = ''; 30 | let y = ''; 31 | for (let i = 0; i < src.length; i += 1) { 32 | if (src.charAt(i) >= '0' && src.charAt(i) <= '9') { 33 | y += src.charAt(i); 34 | } else { 35 | x += src.charAt(i).toUpperCase(); 36 | } 37 | } 38 | return [indexAt(x), parseInt(y, 10) - 1]; 39 | } 40 | 41 | // x,y => B10 42 | export function xy2expr(x, y) { 43 | return `${stringAt(x)}${y + 1}`; 44 | } 45 | 46 | export function expr2expr(src, xn, yn) { 47 | const [x, y] = expr2xy(src); 48 | return xy2expr(x + xn, y + yn); 49 | } 50 | 51 | export default { 52 | stringAt, 53 | indexAt, 54 | expr2xy, 55 | xy2expr, 56 | expr2expr, 57 | }; 58 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/index.js: -------------------------------------------------------------------------------- 1 | import prompt from '@system.prompt'; 2 | import Table from './table/'; 3 | import Viewport from './table/viewport.js'; 4 | 5 | export default { 6 | props: { 7 | sheet: { 8 | type: Array, 9 | default: [] 10 | }, 11 | canvasWidth: { 12 | type: String, 13 | default: '100%', 14 | }, 15 | cavasHeight: { 16 | type: String, 17 | default: '100%', 18 | }, 19 | tableWidth: { 20 | type: Number, 21 | default: 850, 22 | }, 23 | tableHeight: { 24 | type: Number, 25 | default: 800, 26 | }, 27 | }, 28 | data: { 29 | isShowArea: false, 30 | areaTop: 0, 31 | areaLeft: 0, 32 | content: '', 33 | isFocus: true, 34 | }, 35 | touchstart(evt) { 36 | this.$emit('clickCellStart', { 37 | evt, 38 | el: this.el, 39 | textarea: this.textarea, 40 | viewport: this.viewport, 41 | table: this.table, 42 | }); 43 | const cell = this.viewport.cell(evt.touches[0].localX, evt.touches[0].localY); 44 | this.table.$onClick(...cell, evt); 45 | this.setTextarea(...cell, evt); 46 | }, 47 | setTableCell() { 48 | this.table.cell((ri, ci) => { 49 | return this.sheet?.[ri]?.[ci] || ''; 50 | }).render(); 51 | }, 52 | touchend(evt) { 53 | this.$emit('clickCellEnd', { 54 | evt, 55 | el: this.el, 56 | textarea: this.textarea, 57 | viewport: this.viewport, 58 | table: this.table, 59 | }); 60 | const range = this.viewport.range(evt.changedTouches[0].localX, evt.changedTouches[0].localY); 61 | this.table.selection(range); 62 | this.viewport.render(this.table.$draw); 63 | }, 64 | longpress(evt) { 65 | this.$emit('clickCellLongpress'); 66 | }, 67 | change(evt) { 68 | this.$emit('change', { 69 | evt, 70 | el: this.el, 71 | textarea: this.textarea, 72 | viewport: this.viewport, 73 | table: this.table, 74 | }); 75 | this.content = evt.value; 76 | this.sheet[this.row][this.col] = this.content; 77 | this.setTableCell(); 78 | }, 79 | setTextarea(type, cellRect) { 80 | const textarea = this.textarea; 81 | const area = this.area; 82 | this.isShowArea = false; 83 | switch (type) { 84 | case 4: 85 | const { col, height, row, width, x, y } = cellRect; 86 | let value = this.sheet?.[row]?.[col] || ''; 87 | value = typeof value === 'string' || typeof value === 'number' ? 88 | value : value.text 89 | prompt.showToast({ 90 | message: `当前值:${value} 位置:第${row + 1}行 第${col + 1}列`, 91 | duration: 3000, 92 | }); 93 | this.areaLeft = x - 2; 94 | this.areaTop = y - 1; 95 | this.isShowArea = true; 96 | this.row = row; 97 | this.col = col; 98 | this.content = value; 99 | break; 100 | } 101 | }, 102 | onShow() { 103 | this.el = this.$refs.canvas; 104 | this.textarea = this.$refs.textarea; 105 | this.area = this.$refs.area; 106 | 107 | this.table = Table.create(this.el, this.tableWidth, this.tableHeight); 108 | this.setTableCell(); 109 | this.viewport = new Viewport(this.table); 110 | this.$emit('sheetShow', { 111 | el: this.el, 112 | textarea: this.textarea, 113 | viewport: this.viewport, 114 | table: this.table, 115 | }) 116 | }, 117 | onHide() { 118 | this.$emit('sheetHide', { 119 | el: this.el, 120 | textarea: this.textarea, 121 | viewport: this.viewport, 122 | table: this.table, 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/canvas2d.js: -------------------------------------------------------------------------------- 1 | function dpr() { 2 | return 1; 3 | } 4 | 5 | function npx(px) { 6 | return px * dpr(); 7 | } 8 | 9 | function attrRadio(ctx, key, v, cb) { 10 | if (dpr() === 1) cb(); 11 | else { 12 | const old = ctx[key]; 13 | ctx[key] = v(ctx[key]); 14 | cb(); 15 | ctx[key] = old; 16 | } 17 | } 18 | 19 | export default class Canvas2d { 20 | constructor(el) { 21 | this.el = el; 22 | this.ctx = el.getContext('2d'); 23 | } 24 | 25 | resize(width, height) { 26 | const { el } = this; 27 | this.clearRect(0, 0, width, height); 28 | el.style.width = `${width}px`; 29 | el.style.height = `${height}px`; 30 | // for Retina display 31 | el.width = npx(width); 32 | el.height = npx(height); 33 | return this; 34 | } 35 | 36 | attr(options) { 37 | if (typeof options === 'string') { 38 | return this.ctx[options]; 39 | } 40 | Object.entries(options).forEach(([k, v]) => { 41 | if (v !== undefined && v !== null) { 42 | this.ctx[k] = v; 43 | } 44 | }); 45 | return this; 46 | } 47 | 48 | textWidth(txt) { 49 | return this.ctx.measureText(txt).width; 50 | } 51 | 52 | line(...xys) { 53 | if (xys.length > 1) { 54 | this.moveTo(...xys[0]); 55 | for (let i = 1; i < xys.length; i += 1) { 56 | this.lineTo(...xys[i]); 57 | } 58 | this.stroke(); 59 | } 60 | return this; 61 | } 62 | 63 | lines(...xyss) { 64 | if (xyss.length > 0) { 65 | xyss.forEach((xys) => this.line(...xys)); 66 | } 67 | return this; 68 | } 69 | 70 | // style: thin | medium | thick | dashed | dotted 71 | // color: the line color 72 | lineStyle(style, color) { 73 | const { ctx } = this; 74 | ctx.lineWidth = 1; 75 | ctx.strokeStyle = color; 76 | if (style === 'medium') { 77 | ctx.lineWidth = 2; 78 | } else if (style === 'thick') { 79 | ctx.lineWidth = 3; 80 | } else if (style === 'dashed') { 81 | ctx.setLineDash([3, 2]); 82 | } else if (style === 'dotted') { 83 | ctx.setLineDash([1, 1]); 84 | } 85 | return this; 86 | } 87 | 88 | arc(x, y, radius, ...args) { 89 | this.ctx.arc(npx(x), npx(y), npx(radius), ...args); 90 | return this; 91 | } 92 | 93 | roundRect(x, y, w, h, r) { 94 | this.beginPath(); 95 | this.moveTo(x + r, y); 96 | this.arcTo(x + w, y, x + w, y + h, r); 97 | this.arcTo(x + w, y + h, x, y + h, r); 98 | this.arcTo(x, y + h, x, y, r); 99 | this.arcTo(x, y, x + w, y, r); 100 | this.closePath(); 101 | return this; 102 | } 103 | 104 | stroke() { 105 | const { ctx } = this; 106 | attrRadio(ctx, 107 | 'lineWidth', 108 | (it) => npx(it), 109 | () => (ctx.stroke())); 110 | return this; 111 | } 112 | 113 | strokeRect(x, y, width, height) { 114 | const { ctx } = this; 115 | attrRadio(ctx, 116 | 'lineWidth', 117 | (it) => npx(it), 118 | () => (ctx.strokeRect(npx(x), npx(y), width, height))); 119 | return this; 120 | } 121 | 122 | static create(el) { 123 | return new Canvas2d(el); 124 | } 125 | } 126 | 127 | [ 128 | 'save', 'restore', 129 | 'beginPath', 'closePath', 130 | 'clip', 'fill', 'rotate', 131 | ].forEach((it) => { 132 | Canvas2d.prototype[it] = function (...args) { 133 | this.ctx[it](...args); 134 | return this; 135 | }; 136 | }); 137 | 138 | ['fillText', 'strokeText'].forEach((it) => { 139 | Canvas2d.prototype[it] = function (...args) { 140 | args[1] = npx(args[1]); 141 | args[2] = npx(args[2]); 142 | const { ctx } = this; 143 | attrRadio(ctx, 'font', 144 | (font) => font.replace(/([\d|\\.]*)(pt|px)/g, (w, size, u) => `${npx(size)}${u}`), 145 | () => (ctx[it](...args))); 146 | return this; 147 | }; 148 | }); 149 | 150 | // arcTo: 4 151 | Object.entries({ 152 | translate: 2, 153 | rect: 2, 154 | fillRect: 2, 155 | clearRect: 2, 156 | moveTo: 2, 157 | lineTo: 2, 158 | arc: 2, 159 | arcTo: 4, 160 | bezierCurveTo: 6, 161 | quadraticCurveTo: 4, 162 | createRadialGradient: 6, 163 | createLinearGradient: 4, 164 | }).forEach(([it, cnt]) => { 165 | Canvas2d.prototype[it] = function (...args) { 166 | const { ctx } = this; 167 | const even = npx(ctx.lineWidth) % 2 === 0; 168 | ctx[it](...args.map((arg, index) => { 169 | let n = npx(arg); 170 | if (!even && index < cnt) n -= 0.5; 171 | return n; 172 | })); 173 | return this; 174 | }; 175 | }); 176 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/cell-render.js: -------------------------------------------------------------------------------- 1 | // align: left | center | right 2 | // width: the width of cell 3 | // padding: the padding of cell 4 | function textx(align, width, padding) { 5 | switch (align) { 6 | case 'left': 7 | return padding; 8 | case 'center': 9 | return width / 2; 10 | case 'right': 11 | return width - padding; 12 | default: 13 | return 0; 14 | } 15 | } 16 | 17 | // align: top | middle | bottom 18 | // height: the height of cell 19 | // txtHeight: the height of text 20 | // padding: the padding of cell 21 | function texty(align, height, txtHeight, padding) { 22 | switch (align) { 23 | case 'top': 24 | return padding; 25 | case 'middle': 26 | return height / 2 - txtHeight / 2; 27 | case 'bottom': 28 | return height - padding - txtHeight; 29 | default: 30 | return 0; 31 | } 32 | } 33 | 34 | // type: underline | strike 35 | // align: left | center | right 36 | // valign: top | middle | bottom 37 | function textLine(type, align, valign, x, y, w, h) { 38 | // y 39 | let ty = 0; 40 | if (type === 'underline') { 41 | if (valign === 'top') { 42 | ty = -h; 43 | } else if (valign === 'middle') { 44 | ty = -h / 2; 45 | } 46 | } else if (type === 'strike') { 47 | if (valign === 'top') { 48 | ty = -h / 2; 49 | } else if (valign === 'bottom') { 50 | ty = h / 2; 51 | } 52 | } 53 | // x 54 | let tx = 0; 55 | if (align === 'center') { 56 | tx = w / 2; 57 | } else if (align === 'right') { 58 | tx = w; 59 | } 60 | return [ 61 | [x - tx, y - ty], 62 | [x - tx + w, y - ty], 63 | ]; 64 | } 65 | 66 | function renderBorder(draw, width, height, border) { 67 | if (border) { 68 | const { 69 | top, right, bottom, left, 70 | } = border; 71 | draw.save(); 72 | if (top) draw.lineStyle(...top).line([0, 0], [width, 0]); 73 | if (right) draw.lineStyle(...right).line([width, 0], [width, height]); 74 | if (bottom) draw.lineStyle(...bottom).line([0, height], [width, height]); 75 | if (left) draw.lineStyle(...left).line([0, 0], [0, height]); 76 | draw.restore(); 77 | } 78 | } 79 | 80 | function fontString(family, size, italic, bold) { 81 | if (family && size) { 82 | let font = ''; 83 | if (italic) font += 'italic '; 84 | if (bold) font += 'bold '; 85 | return `${font} ${size}pt ${family}`; 86 | } 87 | return undefined; 88 | } 89 | 90 | // draw: Canvas2d 91 | // style: 92 | export function cellRender(draw, text, rect, { 93 | border, fontSize, fontName, 94 | bold, italic, color, bgcolor, 95 | align, valign, underline, strike, 96 | rotate, textwrap, padding, 97 | } = {}) { 98 | // at first move to (left, top) 99 | draw.save().beginPath() 100 | .translate(rect.x, rect.y); 101 | 102 | // border 103 | renderBorder(draw, rect.width, rect.height, border); 104 | 105 | // clip 106 | draw.attr({ fillStyle: bgcolor }) 107 | .rect(0.5, 0.5, rect.width - 1, rect.height - 1) 108 | .clip() 109 | .fill(); 110 | 111 | // text style 112 | draw.save().beginPath().attr({ 113 | textAlign: align, 114 | textBaseline: valign, 115 | font: fontString(fontName, fontSize, italic, bold), 116 | fillStyle: color, 117 | }); 118 | 119 | // rotate 120 | if (rotate && rotate > 0) { 121 | draw.rotate(rotate * (Math.PI / 180)); 122 | } 123 | 124 | const [xp, yp] = padding || [5, 5]; 125 | const tx = textx(align, rect.width, xp); 126 | const txts = text.split('\n'); 127 | const innerWidth = rect.width - (xp * 2); 128 | const ntxts = []; 129 | txts.forEach((it) => { 130 | const txtWidth = draw.textWidth(it); 131 | if (textwrap && txtWidth > innerWidth) { 132 | let txtLine = { w: 0, len: 0, start: 0 }; 133 | for (let i = 0; i < it.length; i += 1) { 134 | if (txtLine.w > innerWidth) { 135 | ntxts.push(it.substr(txtLine.start, txtLine.len)); 136 | txtLine = { w: 0, len: 0, start: i }; 137 | } 138 | txtLine.len += 1; 139 | txtLine.w += draw.textWidth(it[i]) + 1; 140 | } 141 | if (txtLine.len > 0) { 142 | ntxts.push(it.substr(txtLine.start, txtLine.len)); 143 | } 144 | } else { 145 | ntxts.push(it); 146 | } 147 | }); 148 | 149 | const lineHeight = fontSize * 1.425; 150 | const txtHeight = (ntxts.length - 1) * lineHeight; 151 | const lineTypes = []; 152 | if (underline) lineTypes.push('underline'); 153 | if (strike) lineTypes.push('strike'); 154 | let ty = texty(valign, rect.height, txtHeight, yp); 155 | ntxts.forEach((it) => { 156 | const txtWidth = draw.textWidth(it); 157 | draw.fillText(it, tx, ty); 158 | lineTypes.forEach((type) => { 159 | draw.line(...textLine(type, align, valign, tx, ty, txtWidth, fontSize)); 160 | }); 161 | ty += lineHeight; 162 | }); 163 | draw.restore(); 164 | 165 | draw.restore(); 166 | } 167 | 168 | export default {}; 169 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/range.js: -------------------------------------------------------------------------------- 1 | import { expr2xy } from './alphabet'; 2 | 3 | /** 4 | * the range specified by a start position and an end position, 5 | * the smallest range must contain at least one cell. 6 | * Range is not a merged cell, but it can be merged as a single cell 7 | */ 8 | export default class Range { 9 | constructor(startRow, startCol, endRow, endCol) { 10 | // index of row of the start position 11 | this.startRow = startRow; 12 | // index of col of the start position 13 | this.startCol = startCol; 14 | // index of row of the end position 15 | this.endRow = endRow; 16 | // index of col of the end position 17 | this.endCol = endCol; 18 | } 19 | 20 | get start() { 21 | return [this.startRow, this.startCol]; 22 | } 23 | 24 | get end() { 25 | return [this.endRow, this.endCol]; 26 | } 27 | 28 | // count of rows contained in this range 29 | get rows() { 30 | return this.endRow - this.startRow; 31 | } 32 | 33 | // count of cols contained in this range 34 | get cols() { 35 | return this.endCol - this.startCol; 36 | } 37 | 38 | // check whether or not this range is empty 39 | get empty() { 40 | return this.rows() === 0 && this.cols() === 0; 41 | } 42 | 43 | /** 44 | * check whether or not the row-index contained in the row of range 45 | * @param {int} index 46 | * @returns {boolean} 47 | */ 48 | inRow(index) { 49 | return this.startRow <= index && index <= this.endRow; 50 | } 51 | 52 | /** 53 | * check whether or not the index contained in the col of range 54 | * @param {int} index 55 | * @returns {boolean} 56 | */ 57 | inCol(index) { 58 | return this.startCol <= index && index <= this.endCol; 59 | } 60 | 61 | /** 62 | * check whether or not the range contains a cell position(row, col) 63 | * @param {int} row row-index 64 | * @param {int} col col-index 65 | * @returns {boolean} 66 | */ 67 | contains(row, col) { 68 | return this.inRow(row) && this.inCol(col); 69 | } 70 | 71 | /** 72 | * check whether or not the range within the other range 73 | * @param {Range} other 74 | * @returns {boolean} 75 | */ 76 | within(other) { 77 | return this.startRow >= other.startRow 78 | && this.startCol >= other.startCode 79 | && this.endRow <= other.endRow 80 | && this.endCol <= other.endCol; 81 | } 82 | 83 | /** 84 | * check whether or not the range intersects the other range 85 | * @param {Range} other 86 | * @returns {boolean} 87 | */ 88 | intersects(other) { 89 | return this.startRow <= other.endRow 90 | && this.startCol <= other.endCol 91 | && other.startRow <= this.endRow 92 | && other.startCol <= this.endCol; 93 | } 94 | 95 | /** 96 | * the self union the other resulting in the new range 97 | * @param {Range} other 98 | * @returns {Range} the new range 99 | */ 100 | union(other) { 101 | return new Range( 102 | other.startRow < this.startRow ? other.startRow : this.startRow, 103 | other.startCol < this.startCol ? other.startCol : this.startCol, 104 | other.endRow > this.endRow ? other.endRow : this.endRow, 105 | other.endCol > this.endCol ? other.endCol : this.endCol, 106 | ); 107 | } 108 | 109 | /** 110 | * @param {Function} cb (row) => {} 111 | * @returns this 112 | */ 113 | eachRow(cb) { 114 | for (let row = this.startRow; row <= this.endRow; row += 1) { 115 | cb(row); 116 | } 117 | return this; 118 | } 119 | 120 | /** 121 | * @param {Function} cb (col) => {} 122 | * @returns this 123 | */ 124 | eachCol(cb) { 125 | for (let col = this.startCol; col <= this.endCol; col += 1) { 126 | cb(col); 127 | } 128 | return this; 129 | } 130 | 131 | /** 132 | * @param {Function} cb (row, col) => {} 133 | * @returns this 134 | */ 135 | each(cb) { 136 | this.rowEach((row) => { 137 | this.colEach((col) => (cb(row, col))); 138 | }); 139 | return this; 140 | } 141 | 142 | clone() { 143 | return new Range( 144 | this.startRow, 145 | this.startCol, 146 | this.endRow, 147 | this.endCol, 148 | ); 149 | } 150 | } 151 | 152 | export function newRange(ref) { 153 | if (ref === undefined) return undefined; 154 | const ary = ref.split(':'); 155 | const start = expr2xy(ary[0]); 156 | const end = expr2xy(ary[1]); 157 | return new Range(start[1], start[0], end[1], end[0]); 158 | } 159 | 160 | export function eachRanges(refs, cb) { 161 | if (refs && refs.length > 0) { 162 | refs.forEach((ref) => { 163 | cb(newRange(ref)); 164 | }); 165 | } 166 | } 167 | 168 | export function findRanges(refs, cb) { 169 | if (refs && refs.length > 0) { 170 | let it = null; 171 | if (refs.find((ref) => { 172 | it = newRange(ref); 173 | return cb(it); 174 | })) { 175 | return it; 176 | } 177 | } 178 | return null; 179 | } 180 | -------------------------------------------------------------------------------- /entry/src/main/js/default/pages/index/index.js: -------------------------------------------------------------------------------- 1 | import prompt from '@system.prompt'; 2 | 3 | const styleText = { 4 | text: 'Hello World!', 5 | style: { 6 | color: 'blue' 7 | } 8 | }; 9 | export default { 10 | data: { 11 | sheet: [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 12 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 13 | [styleText, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 14 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 15 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 16 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 17 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 18 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 19 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 20 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 21 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 22 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 23 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 24 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 25 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 26 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 27 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 28 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 29 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 30 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 31 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 32 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 33 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 34 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 35 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 36 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 37 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 38 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 39 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 40 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 41 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 42 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 43 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 44 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 45 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 46 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 47 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 48 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]] 49 | }, 50 | clickCellStart(e) { 51 | const evt = e.detail.evt; 52 | }, 53 | clickCellEnd(e) { 54 | const evt = e.detail.evt; 55 | }, 56 | change(e) { 57 | const evt = e.detail.evt; 58 | }, 59 | sheetShow(sheet) { 60 | this.el = sheet.detail.el; 61 | this.textarea = sheet.detail.textarea; 62 | this.viewport = sheet.detail.viewport; 63 | this.table = sheet.detail.table; 64 | }, 65 | setTableCell() { 66 | this.table.cell((ri, ci) => { 67 | return this.sheet?.[ri]?.[ci] || ''; 68 | }).render(); 69 | }, 70 | 71 | dialogSuccess(data) { 72 | switch (data.index) { 73 | case 0: 74 | this.table.cell((ri, ci) => { 75 | if (ri === 2 && ci === 2) { 76 | return { 77 | text: '8848', 78 | style: { 79 | color: 'red' 80 | } 81 | } 82 | } 83 | return this.sheet?.[ri]?.[ci] || ''; 84 | }).render(); 85 | break; 86 | case 1: 87 | this.table.colHeader({ height: 50, rows: 2, merges: ['A1:C1', 'D1:D2'] }) 88 | .merges(['G9:H11', 'B9:D11']) 89 | .freeze('C6') 90 | .render(); 91 | break; 92 | case 2: 93 | this.table 94 | .scrollRows(2) 95 | .scrollCols(1) 96 | .cell((ri, ci) => { 97 | return ''; 98 | }).render(); 99 | break; 100 | } 101 | }, 102 | clickCellLongpress(evt) { 103 | prompt.showDialog({ 104 | buttons: [{ 105 | text: '1. 初始化值,修改3行3列为红色8848', 106 | color: '#666666', 107 | }, { 108 | text: '2. 合并单元格,修改行头部和冻结C6', 109 | color: '#666666', 110 | }, { 111 | text: '3. 修改起始行,清空列表数据', 112 | color: '#666666', 113 | }], 114 | success: this.dialogSuccess, 115 | }); 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/index.js: -------------------------------------------------------------------------------- 1 | import { stringAt, expr2xy } from './alphabet'; 2 | import Canvas2d from './canvas2d'; 3 | import { newRange, eachRanges } from './range'; 4 | import Viewport from './viewport'; 5 | 6 | /** 7 | * ---------------------------------------------------------------- 8 | * | | column header | 9 | * ---------------------------------------------------------------- 10 | * | | | 11 | * | row header | body | 12 | * | | | 13 | * ---------------------------------------------------------------- 14 | * row { height, hide, autoFit } 15 | * col { width, hide, autoFit } 16 | * cell { 17 | * text, 18 | * style: { 19 | * border, fontSize, fontName, 20 | * bold, italic, color, bgcolor, 21 | * align, valign, underline, strike, 22 | * rotate, textwrap, padding, 23 | * }, 24 | * type: text | button | link | checkbox | radio | list | progress | image | imageButton | date 25 | * } 26 | */ 27 | class Table { 28 | // the count of rows 29 | $rows = 100; 30 | 31 | // the count of cols; 32 | $cols = 26; 33 | 34 | /** 35 | * get row given rowIndex 36 | * @param {int} rowIndex 37 | * @returns { height, hide, autoFit } row 38 | */ 39 | $row = () => ({ height: 25, hide: false, autoFit: false }); 40 | 41 | /** 42 | * get col given colIndex 43 | * @param {int} coIndex 44 | * @returns { width, hide, autoFit } col 45 | */ 46 | $col = () => ({ width: 100, hide: false, autoFit: false }); 47 | 48 | /** 49 | * get cell given rowIndex, colIndex 50 | * @param {int} rowIndex 51 | * @param {int} colIndex 52 | * @returns { text, style, type, ...} cell 53 | */ 54 | $cell = () => ''; 55 | 56 | $lineStyle = { 57 | width: 1, 58 | color: '#e6e6e6', 59 | }; 60 | 61 | $cellStyle = { 62 | bgcolor: '#ffffff', 63 | align: 'left', 64 | valign: 'middle', 65 | textwrap: true, 66 | underline: false, 67 | color: '#0a0a0a', 68 | bold: false, 69 | italic: false, 70 | rotate: 0, 71 | fontSize: 9, 72 | fontName: 'Source Sans Pro', 73 | }; 74 | 75 | $merges = []; 76 | 77 | // row header 78 | $rowHeader = { 79 | width: 60, 80 | cell(r) { 81 | return r + 1; 82 | }, 83 | } 84 | 85 | // column header 86 | $colHeader = { 87 | height: 25, 88 | rows: 1, 89 | merges: [], 90 | cell(r, c) { 91 | return stringAt(c); 92 | }, 93 | get rowHeight() { 94 | return this.height / this.rows; 95 | }, 96 | } 97 | 98 | $headerLineStyle = { 99 | width: 1, 100 | color: '#e6e6e6', 101 | }; 102 | 103 | $headerCellStyle = { 104 | bgcolor: '#f4f5f8', 105 | align: 'center', 106 | valign: 'middle', 107 | color: '#585757', 108 | fontSize: 9, 109 | fontName: 'Source Sans Pro', 110 | }; 111 | 112 | // a highlight cell without background filled shows as focused cell 113 | $focus = undefined; 114 | 115 | // The selection range contains multiple cells 116 | $selection = undefined; 117 | 118 | $selectionStyle = { 119 | borderWidth: 2, 120 | borderColor: '#4b89ff', 121 | bgcolor: '#4b89ff14', 122 | }; 123 | 124 | // row of the start position in table 125 | $startRow = 0; 126 | 127 | // col of the start position in table 128 | $startCol = 0; 129 | 130 | // count of rows scrolled 131 | $scrollRows = 0; 132 | 133 | // count of cols scrolled 134 | $scrollCols = 0; 135 | 136 | // freezed cell 137 | $freeze = [0, 0]; 138 | 139 | $freezeLineStyle = { 140 | width: 2, 141 | color: '#d8d8d8', 142 | }; 143 | 144 | /** 145 | * trigger the event by clicking 146 | * @param {int} type 147 | * @param {row, col, x, y, width, height } cellRect 148 | * @param {Event} evt 149 | */ 150 | $onClick = (type) => { }; 151 | 152 | $onSelected = () => { }; 153 | 154 | constructor(container, width, height) { 155 | this.$target = container; 156 | this.$draw = Canvas2d.create(container); 157 | this.$width = width; 158 | this.$height = height; 159 | } 160 | 161 | render() { 162 | this.viewport.render(this.$draw); 163 | return this; 164 | } 165 | 166 | get viewport() { 167 | return new Viewport(this); 168 | } 169 | 170 | // ref: 'A1:B2' | 'A:B' | '1:4' | 'A1' 171 | selection(ref) { 172 | if (typeof ref === 'string') { 173 | this.$selection = newRange(ref); 174 | } else { 175 | this.$selection = ref; 176 | } 177 | this.$focus = this.$selection.start; 178 | return this; 179 | } 180 | 181 | // ref: 'A1:B2' | 'A:B' | '1:4' | 'A1' 182 | freeze(ref) { 183 | if (ref !== 'A1') { 184 | this.$startRow = this.$scrollRows; 185 | this.$startCol = this.$scrollCols; 186 | this.$scrollRows = 0; 187 | this.$scrollCols = 0; 188 | } else { 189 | this.$scrollRows = this.$startRow; 190 | this.$scrollCols = this.$startCol; 191 | this.$startRow = 0; 192 | this.$startCol = 0; 193 | } 194 | this.$freeze = expr2xy(ref).reverse(); 195 | return this; 196 | } 197 | 198 | static create(cssSelector, width, height) { 199 | return new Table(cssSelector, width, height); 200 | } 201 | } 202 | 203 | // single property 204 | [ 205 | 'width', 'height', 'rows', 'cols', 'row', 'col', 'cell', 206 | 'startRow', 'startCol', 'scrollRows', 'scrollCols', 207 | 'merges', 208 | 'onClick', 209 | ].forEach((it) => { 210 | Table.prototype[it] = function (arg) { 211 | this[`$${it}`] = arg; 212 | return this; 213 | }; 214 | }); 215 | 216 | // object property 217 | [ 218 | 'lineStyle', 'cellStyle', 219 | 'headerCellStyle', 'headerLineStyle', 220 | 'selectionStyle', 'freezeLineStyle', 221 | 'rowHeader', 'colHeader', 222 | ].forEach((it) => { 223 | Table.prototype[it] = function (arg) { 224 | Object.assign(this[`$${it}`], arg || {}); 225 | return this; 226 | }; 227 | }); 228 | 229 | export default Table; 230 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ]; do 30 | ls=$(ls -ld "$PRG") 31 | link=$(expr "$ls" : '.*-> \(.*\)$') 32 | if expr "$link" : '/.*' >/dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=$(dirname "$PRG")"/$link" 36 | fi 37 | done 38 | SAVED="$(pwd)" 39 | cd "$(dirname \"$PRG\")/" >/dev/null 40 | APP_HOME="$(pwd -P)" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=$(basename "$0") 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn() { 53 | echo "$*" 54 | } 55 | 56 | die() { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "$(uname)" in 69 | CYGWIN*) 70 | cygwin=true 71 | ;; 72 | Darwin*) 73 | darwin=true 74 | ;; 75 | MINGW*) 76 | msys=true 77 | ;; 78 | NONSTOP*) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ]; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ]; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then 109 | MAX_FD_LIMIT=$(ulimit -H -n) 110 | if [ $? -eq 0 ]; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ]; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ]; then 130 | APP_HOME=$(cygpath --path --mixed "$APP_HOME") 131 | CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") 132 | JAVACMD=$(cygpath --unix "$JAVACMD") 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) 136 | SEP="" 137 | for dir in $ROOTDIRSRAW; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ]; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@"; do 149 | CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) 150 | CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition 153 | eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") 154 | else 155 | eval $(echo args$i)="\"$arg\"" 156 | fi 157 | i=$(expr $i + 1) 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save() { 175 | for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/render.js: -------------------------------------------------------------------------------- 1 | import { cellRender } from './cell-render'; 2 | import { eachRanges } from './range'; 3 | import { newArea } from './area'; 4 | 5 | /** 6 | * render the grid lines 7 | * @param {Canvas2d} draw 8 | * @param {Area} area 9 | * @param {width, color} param2 the line style 10 | */ 11 | function renderLines(draw, area, { width, color }) { 12 | // render row-col-lines 13 | if (width > 0) { 14 | draw.save().beginPath() 15 | .attr({ lineWidth: width, strokeStyle: color }); 16 | 17 | area.eachRow((ri, v) => { 18 | const h = v.y + v.height; 19 | draw.line([0, h], [area.width, h]); 20 | }); 21 | 22 | area.eachCol((ci, v) => { 23 | const w = v.x + v.width; 24 | draw.line([w, 0], [w, area.height]); 25 | }); 26 | draw.restore(); 27 | } 28 | } 29 | 30 | /** 31 | * render cell given params 32 | * @param {Canvas2d} draw 33 | * @param {int} ri the row index 34 | * @param {int} ci the col index 35 | * @param {Function} cell { text, style, type ...} 36 | * @param {x, y, width, height} cellRect 37 | * @param {style} cellStyle the style of default-cell 38 | */ 39 | function renderCell(draw, ri, ci, cell, cellRect, cellStyle) { 40 | const c = cell(ri, ci); 41 | let text = ''; 42 | let style = cellStyle; 43 | if (c !== undefined) { 44 | if (typeof c === 'string' || typeof c === 'number') text = `${c}`; 45 | else { 46 | text = c.text || ''; 47 | if (c.style) { 48 | style = { ...style, ...c.style }; 49 | } 50 | } 51 | } 52 | cellRender(draw, text, cellRect, style); 53 | } 54 | 55 | /** 56 | * render cells 57 | * @param {Canvas2d} draw 58 | * @param {string} type 'row-header' | 'col-header' | 'body' 59 | * @param {Area} area 60 | * @param {Function} cell 61 | * @param {style} cellStyle 62 | * @param {Range} selection 63 | * @param {style} selectionStyle 64 | * @param {Array} merges 65 | */ 66 | function renderCells(draw, type, area, cell, cellStyle, selection, selectionStyle, merges) { 67 | draw.save().rect(0, 0, area.width, area.height).clip(); 68 | area.each((ri, ci, rect) => { 69 | renderCell(draw, ri, ci, cell, rect, cellStyle); 70 | }); 71 | 72 | // render merges 73 | eachRanges(merges, (it) => { 74 | if (it.intersects(area)) { 75 | renderCell(draw, it.startRow, it.startCol, 76 | cell, area.rect(it), cellStyle); 77 | } 78 | }); 79 | 80 | // render selection 81 | if (selection && area.intersects(selection)) { 82 | const { 83 | x, y, width, height, 84 | } = area.rect(selection); 85 | const { bgcolor, borderWidth, borderColor } = selectionStyle; 86 | const bw = type === 'body' ? borderWidth : 0; 87 | draw.save() 88 | .attr({ fillStyle: bgcolor }) 89 | .rect(x + bw / 2, y + bw / 2, width - bw, height - bw) 90 | .fill(); 91 | if (type === 'body') { 92 | draw.attr({ 93 | strokeStyle: borderColor, 94 | lineWidth: borderWidth, 95 | }).stroke(); 96 | } 97 | draw.restore(); 98 | } 99 | draw.restore(); 100 | } 101 | 102 | function renderLinesAndCells(draw, type, area, 103 | cell, cellStyle, lineStyle, selection, selectionStyle, merges) { 104 | renderLines(draw, area, lineStyle); 105 | renderCells(draw, type, area, cell, cellStyle, 106 | selection, selectionStyle, merges); 107 | } 108 | 109 | // private methods --- start ---- 110 | 111 | function renderRowHeader(draw, area) { 112 | const { cell, width } = this.$rowHeader; 113 | // render row-index 114 | if (width > 0) { 115 | draw.save().translate(0, area.y); 116 | const { $selection } = this; 117 | let nselection = null; 118 | if ($selection) { 119 | nselection = this.$selection.clone(); 120 | nselection.startCol = 0; 121 | nselection.endCol = 0; 122 | } 123 | renderLinesAndCells(draw, 'row-header', area, 124 | cell, this.$headerCellStyle, this.$headerLineStyle, 125 | nselection, this.$selectionStyle); 126 | draw.restore(); 127 | } 128 | } 129 | 130 | function renderColHeader(draw, area) { 131 | const { cell, height, merges } = this.$colHeader; 132 | // render col-index 133 | if (height > 0) { 134 | draw.save().translate(area.x, 0); 135 | const { $selection } = this; 136 | let nselection = null; 137 | if ($selection) { 138 | nselection = this.$selection.clone(); 139 | nselection.startRow = 0; 140 | nselection.endRow = area.endRow; 141 | } 142 | renderLinesAndCells(draw, 'col-header', area, 143 | cell, this.$headerCellStyle, this.$headerLineStyle, 144 | nselection, this.$selectionStyle, merges); 145 | draw.restore(); 146 | } 147 | } 148 | 149 | function renderBody(draw, area) { 150 | draw.save().translate(area.x, area.y); 151 | renderLinesAndCells(draw, 'body', area, 152 | this.$cell, this.$cellStyle, this.$lineStyle, 153 | this.$selection, this.$selectionStyle, this.$merges); 154 | draw.restore(); 155 | } 156 | 157 | function renderFreezeLines(draw, x, y) { 158 | const [fr, fc] = this.$freeze; 159 | const { width, color } = this.$freezeLineStyle; 160 | if (width > 0 && (fr > 0 || fc > 0)) { 161 | draw.save().beginPath().attr({ lineWidth: width, strokeStyle: color }); 162 | if (fr > 0) draw.line([0, y], [this.$width, y]); 163 | if (fc > 0) draw.line([x, 0], [x, this.$height]); 164 | draw.restore(); 165 | } 166 | } 167 | 168 | export function render(draw, 169 | [area1, area2, area3, area4], 170 | [iarea1, iarea21, iarea23, iarea3]) { 171 | draw.resize(this.$width, this.$height); 172 | 173 | // render area-4 174 | renderBody.call(this, draw, area4); 175 | 176 | // render area-1 177 | renderBody.call(this, draw, area1); 178 | renderColHeader.call(this, draw, iarea1); 179 | 180 | // render area-3 181 | renderBody.call(this, draw, area3); 182 | renderRowHeader.call(this, draw, iarea3); 183 | 184 | // render area-2 185 | renderBody.call(this, draw, area2); 186 | renderColHeader.call(this, draw, iarea21); 187 | renderRowHeader.call(this, draw, iarea23); 188 | 189 | // render freeze 190 | renderFreezeLines.call(this, draw, area4.x, area4.y); 191 | 192 | // left-top 193 | const { x, y } = area2; 194 | if (x > 0 && y > 0) { 195 | renderLinesAndCells(draw, 'header', 196 | newArea(0, 0, 0, 0, () => ({ width: x }), () => ({ height: y })), 197 | () => '', this.$headerCellStyle, this.$headerLineStyle); 198 | } 199 | } 200 | 201 | export default {}; 202 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/viewport.js: -------------------------------------------------------------------------------- 1 | import { endCell, eachRange, newArea } from './area'; 2 | import Range, { findRanges } from './range'; 3 | import { render } from './render'; 4 | 5 | function $newArea(startRow, startCol, endRow, endCol, x, y) { 6 | return newArea(startRow, startCol, endRow, endCol, 7 | this.$col, this.$row, x, y); 8 | } 9 | 10 | // | 11 | // 2(top-left) | 1(top-right) 12 | // ------------------------------------ 13 | // 3(bottom-left) | 4(bottom-right) 14 | // | 15 | function $newBodyAreas() { 16 | const { 17 | $startRow, $startCol, $width, $height, 18 | $colHeader, $rowHeader, $scrollRows, $scrollCols, 19 | $freeze, $row, $col, $rows, $cols, 20 | } = this; 21 | const tx = $rowHeader.width; 22 | const ty = $colHeader.height; 23 | const [fr, fc] = $freeze; 24 | const area2 = $newArea.call(this, $startRow, $startCol, fr - 1, fc - 1, tx, ty); 25 | const startRow4 = fr + $scrollRows; 26 | const startCol4 = fc + $scrollCols; 27 | const { row, col } = endCell($row, $col, 28 | startRow4, startCol4, $rows, $cols, 29 | area2.width, area2.height, $width, $height); 30 | const area4 = $newArea.call(this, startRow4, startCol4, row, col, 31 | tx + area2.width, ty + area2.height); 32 | const area1 = $newArea.call(this, $startRow, startCol4, fr - 1, col, 33 | tx + area2.width, ty + 0); 34 | const area3 = $newArea.call(this, startRow4, $startCol, row, fc - 1, 35 | tx + 0, ty + area2.height); 36 | return [area1, area2, area3, area4]; 37 | } 38 | 39 | // return [1, 2-1, 2-3, 3] 40 | function $newHeaderAreas([area1, area2, area3, area4]) { 41 | const { 42 | $colHeader, $rowHeader, $row, $col, 43 | } = this; 44 | const columnHeaderRows = $colHeader.rows - 1; 45 | return [ 46 | // 1 47 | newArea(0, area1.startCol, columnHeaderRows, area1.endCol, 48 | $col, () => ({ height: $colHeader.rowHeight }), area4.x, 0), 49 | // 2-1 50 | newArea(0, area2.startCol, columnHeaderRows, area2.endCol, 51 | $col, () => ({ height: $colHeader.rowHeight }), area2.x, 0), 52 | // 2-3 53 | newArea(area2.startRow, 0, area2.endRow, 0, 54 | () => ({ width: $rowHeader.width }), $row, 0, area2.y), 55 | // 3 56 | newArea(area3.startRow, 0, area3.endRow, 0, 57 | () => ({ width: $rowHeader.width }), $row, 0, area4.y), 58 | ]; 59 | } 60 | 61 | function rangeInAreas([area1, area2, area3, area4], 62 | [iarea1, iarea21, iarea23, iarea3], x, y) { 63 | const { $merges, $rows, $cols } = this; 64 | const inIndexRows = x < area2.x; 65 | const inIndexCols = y < area2.y; 66 | const range = new Range(0, 0, $rows - 1, $cols - 1); 67 | const cellfn = (a) => a.cell(x, y); 68 | if (inIndexRows && inIndexCols) { 69 | return range; 70 | } 71 | 72 | 73 | if (inIndexRows) { 74 | const r = cellfn(iarea23.iny(y) ? iarea23 : iarea3).row; 75 | range.startRow = r; 76 | range.endRow = r; 77 | return range; 78 | } 79 | 80 | if (inIndexCols) { 81 | const c = cellfn(iarea21.inx(x) ? iarea21 : iarea1).col; 82 | range.startCol = c; 83 | range.endCol = c; 84 | return range; 85 | } 86 | 87 | const ary = [area4, area2, area1, area3]; 88 | for (let i = 0; i < ary.length; i += 1) { 89 | const area = ary[i]; 90 | if (area.inxy(x, y)) { 91 | const { row, col } = cellfn(area); 92 | const cr = findRanges($merges, (it) => it.contains(row, col)); 93 | if (cr) return cr; 94 | return new Range(row, col, row, col); 95 | } 96 | } 97 | return null; 98 | } 99 | 100 | // 2 | 1 101 | // ------- 102 | // 3 | 4 103 | // return [type, {row, col, x, y, width, height }, evt] 104 | function cellInAreas([area1, area2, area3, area4], 105 | [iarea1, iarea21, iarea23, iarea3], x, y) { 106 | const { $merges, $row, $col } = this; 107 | const inIndexRows = x < area2.x; 108 | const inIndexCols = y < area2.y; 109 | // const { $indexColWidth } = this; 110 | if (inIndexRows && inIndexCols) { 111 | return [2, { 112 | row: 0, col: 0, x: 0, y: 0, width: area2.x, height: area2.y, 113 | }]; 114 | } 115 | 116 | const cellfn = (a) => { 117 | let ret = a.cell(x, y); 118 | const cr = findRanges($merges, (it) => it.contains(ret.row, ret.col)); 119 | if (cr) { 120 | const { 121 | startRow, startCol, endRow, endCol, 122 | } = cr; 123 | const gap = { width: 0, height: 0 }; 124 | eachRange(area2.endRow + 1, area4.startRow - 1, $row, (i, { height }) => { 125 | if (i <= endRow) gap.height += height; 126 | }); 127 | eachRange(area2.endCol + 1, area4.startCol - 1, $col, (i, { width }) => { 128 | if (i <= endCol) gap.width += width; 129 | }); 130 | if (area2.contains(startRow, startCol)) { 131 | ret = area2.rect(cr, true); 132 | ret.width -= gap.width; 133 | ret.height -= gap.height; 134 | } else if (area1.contains(startRow, startCol)) { 135 | ret = area1.rect(cr, true); 136 | ret.height -= gap.height; 137 | } else if (area3.contains(startRow, startCol)) { 138 | ret = area3.rect(cr, true); 139 | ret.width -= gap.width; 140 | } else { 141 | ret = area4.rect(cr, true); 142 | } 143 | return { 144 | row: startRow, 145 | col: startCol, 146 | ...ret, 147 | }; 148 | } 149 | return ret; 150 | }; 151 | 152 | if (inIndexRows) { 153 | if (iarea23.iny(y)) { 154 | return [3, cellfn(iarea23)]; 155 | } 156 | return [3, cellfn(iarea3)]; 157 | } 158 | if (inIndexCols) { 159 | if (iarea21.inx(x)) { 160 | return [1, cellfn(iarea21)]; 161 | } 162 | return [1, cellfn(iarea1)]; 163 | } 164 | const ary = [area4, area2, area1, area3]; 165 | for (let i = 0; i < ary.length; i += 1) { 166 | const area = ary[i]; 167 | if (area.inxy(x, y)) { 168 | return [4, cellfn(area)]; 169 | } 170 | } 171 | return null; 172 | } 173 | 174 | /** 175 | * it contains header(areas), body(areas) with table 176 | */ 177 | export default class Viewport { 178 | constructor(table) { 179 | this.table = table; 180 | this.body = $newBodyAreas.call(table); 181 | this.header = $newHeaderAreas.call(table, this.body); 182 | } 183 | 184 | cell(x, y) { 185 | return cellInAreas.call(this.table, this.body, this.header, x, y); 186 | } 187 | 188 | range(x, y) { 189 | return rangeInAreas.call(this.table, this.body, this.header, x, y); 190 | } 191 | 192 | render(draw) { 193 | render.call(this.table, draw, this.body, this.header); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /entry/src/main/js/default/components/table/area.js: -------------------------------------------------------------------------------- 1 | import Range from './range'; 2 | 3 | /** 4 | * each range of row or col 5 | * @param {int} min the min value 6 | * @param {int} max the max value 7 | * @param {Function} getv (index) => v 8 | * @param {Function} cb (index, v) => {} 9 | */ 10 | export function eachRange(min, max, getv, cb) { 11 | for (let i = min; i <= max; i += 1) { 12 | const v = getv(i); 13 | if (v.hide !== true) cb(i, v); 14 | } 15 | } 16 | 17 | /** 18 | * get the end row given params... 19 | * @param {Function} row (index) => { height, hide } 20 | * @param {int} minRow the min row 21 | * @param {int} maxRow the max row 22 | * @param {int} miny the min value on y-axis 23 | * @param {int} maxy the max value on y-axis 24 | */ 25 | function endCellRow(row, minRow, maxRow, miny, maxy) { 26 | let r = minRow; 27 | let y = miny; 28 | let lasth = 0; 29 | while (y < maxy && r <= maxRow) { 30 | const { height, hide } = row(r); 31 | if (hide !== true) { 32 | lasth = height; 33 | y += height; 34 | } 35 | r += 1; 36 | } 37 | y -= lasth; 38 | return { row: r - 1, y, height: lasth }; 39 | } 40 | 41 | /** 42 | * get the end col given params... 43 | * @param {Function} col (index) => { width, hide } 44 | * @param {int} minCol the min col 45 | * @param {int} maxCol the max col 46 | * @param {int} minx the min value on x-axis 47 | * @param {int} maxx the max value on x-axis 48 | */ 49 | function endCellCol(col, minCol, maxCol, minx, maxx) { 50 | let c = minCol; 51 | let x = minx; 52 | let lastw = 0; 53 | while (x < maxx && c <= maxCol) { 54 | const { width, hide } = col(c); 55 | if (hide !== true) { 56 | lastw = width; 57 | x += width; 58 | } 59 | c += 1; 60 | } 61 | x -= lastw; 62 | return { col: c - 1, x, width: lastw }; 63 | } 64 | 65 | export function endCell(row, col, 66 | minRow, minCol, maxRow, maxCol, 67 | minx, miny, maxx, maxy) { 68 | return { 69 | ...endCellRow(row, minRow, maxRow, miny, maxy), 70 | ...endCellCol(col, minCol, maxCol, minx, maxx), 71 | }; 72 | } 73 | 74 | /** 75 | * it's a range with { x, y, width, height } 76 | * and calculating height of row and width of col.. 77 | */ 78 | export default class Area extends Range { 79 | constructor(startRow, startCol, endRow, endCol, col, row, x = 0, y = 0) { 80 | super(startRow, startCol, endRow, endCol); 81 | // the function of returning { width, hide } 82 | this.$col = col; 83 | // the function of returning { height, hide } 84 | this.$row = row; 85 | 86 | // cache with row and col 87 | // { rowIndex: { height, hide }} 88 | this.$rowMap = new Map(); 89 | // { colIndex: { width, hide }} 90 | this.$colMap = new Map(); 91 | 92 | this.x = x; 93 | this.y = y; 94 | this.width = 0; 95 | this.height = 0; 96 | 97 | eachRange(startRow, endRow, (i) => row(i), (i, { height }) => { 98 | this.$rowMap.set(i, { y: this.height, height }); 99 | this.height += height; 100 | }); 101 | eachRange(startCol, endCol, (i) => col(i), (i, { width }) => { 102 | this.$colMap.set(i, { x: this.width, width }); 103 | this.width += width; 104 | }); 105 | } 106 | 107 | /** 108 | * get {y, height} given index, endIndex 109 | * y: the offset on y-axis 110 | * @param {int} index 111 | * @param {int} endIndex (option param) 112 | * @returns {y, height} 113 | * @example 114 | * row(5) 115 | * row(5, 10) 116 | */ 117 | row(index, endIndex) { 118 | const { $rowMap, startRow, $row } = this; 119 | if ((endIndex === undefined || index === endIndex) && $rowMap.has(index)) { 120 | return $rowMap.get(index); 121 | } 122 | if (index < startRow) { 123 | let y = 0; 124 | let height = 0; 125 | eachRange(index, endIndex, (i) => $row(i), (i, v) => { 126 | if (i < startRow) y -= v.height; 127 | height += v.height; 128 | }); 129 | return { y, height }; 130 | } 131 | const { y } = $rowMap.get(index); 132 | let height = 0; 133 | eachRange(index, endIndex, (i) => $row(i), (i, v) => { 134 | height += v.height; 135 | }); 136 | return { y, height }; 137 | } 138 | 139 | /** 140 | * get {x, width} given index, endIndex 141 | * x: the offset on y-axis 142 | * @param {int} index 143 | * @param {int} endIndex (option param) 144 | * @returns {x, width} 145 | */ 146 | col(index, endIndex) { 147 | const { $colMap, startCol, $col } = this; 148 | if ((endIndex === undefined || index === endIndex) && $colMap.has(index)) { 149 | return $colMap.get(index); 150 | } 151 | if (index < startCol) { 152 | let x = 0; 153 | let width = 0; 154 | eachRange(index, endIndex, (i) => $col(i), (i, v) => { 155 | if (i < startCol) x -= v.width; 156 | width += v.width; 157 | }); 158 | return { x, width }; 159 | } 160 | const { x } = $colMap.get(index); 161 | let width = 0; 162 | eachRange(index, endIndex, (i) => $col(i), (i, v) => { 163 | width += v.width; 164 | }); 165 | return { x, width }; 166 | } 167 | 168 | /** 169 | * check whether or not x contained in area 170 | * @param {int} x offset on x-axis 171 | */ 172 | inx(x) { 173 | return x >= this.x && x < (this.x + this.width); 174 | } 175 | 176 | /** 177 | * check whether or not y contained in area 178 | * @param {int} y offset on y-axis 179 | */ 180 | iny(y) { 181 | return y >= this.y && y < (this.y + this.height); 182 | } 183 | 184 | /** 185 | * check whether or not the tabe contains (x, y) 186 | * @param {int} x offset on x-axis 187 | * @param {int} y offset on y-axis 188 | */ 189 | inxy(x, y) { 190 | return this.inx(x) && this.iny(y); 191 | } 192 | 193 | /** 194 | * get cell given x, y in canvas!!!! 195 | * @param {int} x offset on x-axis 196 | * @param {int} y offset on y-axis 197 | * @returns { row, col, x, y, width, height } 198 | */ 199 | cell(x, y) { 200 | return endCell(this.$row, this.$col, 201 | this.startRow, this.startCol, this.endRow, this.endCol, 202 | this.x, this.y, x, y); 203 | } 204 | 205 | /** 206 | * get rect given range in area 207 | * @param {Range} range 208 | * @param {boolean} isCanvas 209 | * @returns { x, y, width, height } 210 | */ 211 | rect(range, inCanvas = false) { 212 | const c = this.col(range.startCol, range.endCol); 213 | const r = this.row(range.startRow, range.endRow); 214 | if (inCanvas) { 215 | c.x += this.x; 216 | r.y += this.y; 217 | } 218 | return { 219 | ...c, ...r, 220 | }; 221 | } 222 | 223 | eachRow(cb) { 224 | eachRange(this.startRow, this.endRow, 225 | (i) => this.$rowMap.get(i), (i, v) => cb(i, v)); 226 | } 227 | 228 | eachCol(cb) { 229 | eachRange(this.startCol, this.endCol, 230 | (i) => this.$colMap.get(i), (i, v) => cb(i, v)); 231 | } 232 | 233 | each(cb) { 234 | this.eachRow((ri, { y, height }) => { 235 | this.eachCol((ci, { x, width }) => { 236 | cb(ri, ci, { 237 | x, y, width, height, 238 | }); 239 | }); 240 | }); 241 | } 242 | } 243 | 244 | export function newArea(...args) { 245 | return new Area(...args); 246 | } 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenHarmonySheet 2 | 3 | 基于 `Canvas` 实现的高性能 `Excel` 表格引擎组件 [OpenHarmonySheet](https://github.com/Wscats/sheet)。 4 | 5 | 由于大部分前端项目渲染层是使用框架根据排版模型树结构逐层渲染的,整棵渲染树也是与排版模型树一一对应。因此,整个渲染的节点也非常多。项目较大时,性能会受到较大的影响。 6 | 7 | 为了提升渲染性能,提供更优质的编辑体验从 `DOM` 更换成 `Canvas` 渲染,方便开发者构建重前端大型在线文档项目,**在国内外实现类似引擎的公司仅仅只有几家**,如:腾讯文档,金山文档和谷歌文档等。 8 | 9 | 10 | 11 | 在项目中引入 `` 组件即可,使用方法如下: 12 | 13 | ```html 14 | 15 | 16 | 25 | ``` 26 | 27 | # 生命周期和事件 28 | 29 | - sheet 表格数据 30 | - @sheet-show 表格显示 31 | - @sheet-hide 表格隐藏 32 | - @click-cell-start 单元格点击前 33 | - @click-cell-end 单元格点击后 34 | - @click-cell-longpress 长按表格 35 | - @change 修改单元格数据 36 | 37 | 比如,我们在示例中可以监听 `长按` 事件,当用户 `长按` 的时候弹出 `对话框`,示例代码如下: 38 | 39 | ```ts 40 | clickCellLongpress(evt) { 41 | prompt.showDialog({ 42 | buttons: [{ 43 | text: '测试', 44 | color: '#666666', 45 | }], 46 | }); 47 | } 48 | ``` 49 | 50 | 以上所有的接口都会返回一个详细的 `sheet` 对象,里面含有以下信息: 51 | 52 | - el 表格的节点 53 | - textarea 单元格输入框节点 54 | - viewport 单元格高亮选框 55 | - table 单元格操作对象 56 | 57 | ```ts 58 | sheetShow(sheet) { 59 | this.el = sheet.detail.el; 60 | this.textarea = sheet.detail.textarea; 61 | this.viewport = sheet.detail.viewport; 62 | this.table = sheet.detail.table; 63 | } 64 | ``` 65 | 66 | # API 接口 67 | 68 | 渲染引擎封装好了常用的表格数据操作等接口。 69 | 70 | - `this.table.xxx` 71 | 72 | 用于帮助你操作单元格的所有数据和格式,也极大方便你自定义一个功能完整的工具栏: 73 | 74 | 75 | 76 | - `this.viewport.xxx` 77 | 78 | 用于帮助你操作单元格上层的高亮选框。 79 | 80 | - `this.textarea.xxx` 81 | 82 | `this.textarea` 是对鸿蒙的原生 `