├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── scripts
├── build.js
├── rollup.config.js
└── util.js
├── src
├── AppSwitcher
│ ├── cssText.ts
│ └── index.ts
├── Applet
│ ├── Base.ts
│ ├── EventTarget.ts
│ ├── LifeCycle.ts
│ ├── Prefetch.ts
│ ├── State.ts
│ ├── View.ts
│ ├── captureShot
│ │ └── index.ts
│ ├── index.ts
│ ├── inject
│ │ ├── cssVar.ts
│ │ ├── index.ts
│ │ ├── openWindow.ts
│ │ ├── smartSetTimeout.ts
│ │ └── tapHighlight.ts
│ └── manifestProcess.ts
├── AppletControls
│ ├── Base.ts
│ ├── EventTarget.ts
│ ├── State.ts
│ ├── View.ts
│ ├── cssText.ts
│ └── index.ts
├── Application
│ ├── Base.ts
│ ├── State.ts
│ ├── index.ts
│ └── provider.ts
├── Define
│ ├── DefineApplet.ts
│ ├── DefineApplication.ts
│ ├── env.ts
│ ├── index.ts
│ ├── init.ts
│ ├── prepare.ts
│ ├── preset.ts
│ └── slot.ts
├── Event
│ └── index.ts
├── Modality
│ ├── Base.ts
│ ├── EventTarget.ts
│ ├── State.ts
│ ├── View.ts
│ ├── cssText.ts
│ └── index.ts
├── Sandbox
│ └── index.ts
├── Scroll
│ ├── index.ts
│ ├── polyfill.ts
│ └── types.module.ts
├── Segue
│ ├── Animation.ts
│ ├── Base.ts
│ ├── History.ts
│ ├── State.ts
│ ├── Switch.ts
│ ├── index.ts
│ └── preset
│ │ ├── fade.ts
│ │ ├── flip.ts
│ │ ├── grow.ts
│ │ ├── popup.ts
│ │ ├── slide-native.ts
│ │ ├── slide.ts
│ │ └── zoom.ts
├── Slide
│ ├── Base.ts
│ ├── EventTarget.ts
│ ├── State.ts
│ ├── View.ts
│ ├── cssText.ts
│ └── index.ts
├── global.d.ts
├── index.ts
├── launcher.ts
├── lib
│ ├── cssText
│ │ ├── coveredCSSText.ts
│ │ ├── fullscreenBaseCSSText.ts
│ │ └── globalCSSText.ts
│ ├── typeError
│ │ ├── errorCode.ts
│ │ ├── index.ts
│ │ └── type.d.ts
│ ├── util
│ │ ├── FrameElapsedDuration.ts
│ │ ├── FrameQueue.ts
│ │ ├── getCSSUnits.ts
│ │ ├── getIOSVersion.ts
│ │ ├── index.ts
│ │ ├── requestAnimationFrame.ts
│ │ ├── requestIdleCallback.ts
│ │ ├── setInterval.ts
│ │ ├── setTimeout.ts
│ │ ├── sleep.ts
│ │ ├── testHasScrolling.ts
│ │ ├── testHasSlotBug.ts
│ │ ├── testHasSmoothScrolling.ts
│ │ ├── testHasSmoothSnapScrolling.ts
│ │ └── testHasSnapReset.ts
│ ├── wc
│ │ ├── needsPolyfill.ts
│ │ └── shim
│ │ │ └── native-shim.ts
│ └── webAnimations
│ │ ├── clear.ts
│ │ ├── ease.ts
│ │ ├── finish.ts
│ │ ├── load.ts
│ │ ├── polyfill.ts
│ │ └── reset.ts
├── pullToRefresh
│ ├── cssText.ts
│ └── index.ts
└── types.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 | charset = utf-8
12 | indent_style = space
13 | indent_size = 4
14 |
15 | # Matches the exact files either *.json or *.yml
16 | [*.{json,yml}]
17 | indent_style = space
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # don't ever lint node_modules
2 | node_modules
3 | # don't lint build output (make sure it's set to your correct build folder name)
4 | app
5 | scripts
6 | .eslintrc.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | plugins: [
5 | '@typescript-eslint',
6 | ],
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:@typescript-eslint/recommended',
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | *~
4 | *.swp
5 | *.log
6 |
7 | .DS_Store
8 | .temp/
9 |
10 | build/
11 | app
12 | node_modules/
13 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | ./src
2 | ./scripts
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "backout",
4 | "filedir",
5 | "funs",
6 | "iframe",
7 | "ioing",
8 | "jsnext",
9 | "onwarn",
10 | "oversaturated",
11 | "prelink",
12 | "shadowroot",
13 | "subc",
14 | "subviews",
15 | "Viewports",
16 | "webcomponents",
17 | "webcomponentsjs"
18 | ]
19 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 lien
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduce
2 |
3 | A pure front-end container
4 |
5 | Bring interactive experiences comparable to Native Apps.
6 |
7 | https://lath.dev
8 |
9 | # Install:
10 |
11 | ```bash
12 | $ npm i lath --save
13 | ```
14 |
15 | # Example
16 | https://github.com/ioing/lath-vue-example
17 |
18 | # Use
19 |
20 | ```html
21 |
22 |
23 |
24 |
25 |
26 | I am FrameworksApplets
27 |
28 |
29 |
30 |
31 | I am home
32 |
33 |
34 |
35 |
36 | I am pageA
37 |
38 |
39 |
40 |
41 | ```
42 |
43 | ```ts
44 | import { createApplication } from 'lath'
45 | /**
46 | * "home": moduleName
47 | * "root": target element id
48 | */
49 | createApplication({
50 | applets: {
51 | frameworks: {...}, // frameworks module config
52 | home: {...} // normal module config
53 | pageA: {...} // normal module config
54 | pageC: {
55 | config: {
56 | source: {
57 | src: '/c.html'
58 | }
59 | },
60 | ...
61 | } // normal module config
62 | }
63 | }).then(( application ) => {
64 | console.log(application)
65 | })
66 | ```
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lath",
3 | "version": "0.12.40",
4 | "description": "Seamless connection of pages.",
5 | "keywords": [
6 | "lath",
7 | "portal",
8 | "web-portal",
9 | "pages-link",
10 | "linkup",
11 | "seamless",
12 | "pushWindow",
13 | "slide page",
14 | "page animate",
15 | "iframe",
16 | "SPA",
17 | "PWA",
18 | "Application",
19 | "webapp",
20 | "Web Application",
21 | "Web Components",
22 | "Single Page Web Application",
23 | "The Progressive JavaScript Framework"
24 | ],
25 | "main": "app/launcher.js",
26 | "module": "app/launcher.js",
27 | "jsnext:main": "app/launcher.js",
28 | "types": "app/typings/launcher.d.ts",
29 | "files": [
30 | "app/"
31 | ],
32 | "scripts": {
33 | "clean": "rimraf ./app",
34 | "lint": "eslint --cache --ext .ts ./src",
35 | "build": "node scripts/build.js",
36 | "debug": "node --inspect-brk app/index.js"
37 | },
38 | "engines": {
39 | "install-node": ">=14.18.3",
40 | "npm": ">=7.0.0"
41 | },
42 | "dependencies": {
43 | "@webcomponents/custom-elements": "^1.5.0",
44 | "@webcomponents/webcomponentsjs": "^2.6.0",
45 | "html2canvas": "^1.4.1",
46 | "scroll-polyfill": "^1.0.1",
47 | "web-animations-js": "^2.3.2"
48 | },
49 | "devDependencies": {
50 | "@types/node": "^10.12.12",
51 | "@types/web-animations-js": "^2.2.13",
52 | "@typescript-eslint/eslint-plugin": "^5.11.0",
53 | "@typescript-eslint/parser": "^5.11.0",
54 | "eslint": "^8.11.0",
55 | "rimraf": "^3.0.2",
56 | "rollup": "^2.57.0",
57 | "rollup-plugin-typescript2": "^0.31.2",
58 | "ts-loader": "^9.2.6",
59 | "tslib": "^2.3.1",
60 | "uglify-js": "^3.17.4",
61 | "typescript": "^4.6.2"
62 | },
63 | "author": "lien",
64 | "license": "MIT"
65 | }
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const sh = require('./util').sh
2 | const fs = require('fs')
3 | const path = require('path')
4 | const filePath = path.resolve('./app')
5 |
6 | function uglifyJs(filePath) {
7 | fs.readdir(filePath, function (err, files) {
8 | if (err) {
9 | console.warn(err)
10 | } else {
11 | files.forEach(function (filename) {
12 | const filedir = path.join(filePath, filename)
13 | fs.stat(filedir, function (error, stats) {
14 | const isFile = stats.isFile()
15 | const isDir = stats.isDirectory()
16 | if (isFile) {
17 | sh(`npx uglify-js ${filedir} \
18 | -c hoist_funs,hoist_vars \
19 | -m \
20 | -o ${filedir}`)
21 | }
22 | if (isDir && filename.indexOf('typings') !== 0) {
23 | uglifyJs(filedir)
24 | }
25 | })
26 | })
27 | }
28 | })
29 | }
30 |
31 | async function build() {
32 | await sh('npm run clean && npx rollup -c scripts/rollup.config.js')
33 | uglifyJs(filePath)
34 | }
35 |
36 | build()
37 |
--------------------------------------------------------------------------------
/scripts/rollup.config.js:
--------------------------------------------------------------------------------
1 | const typescript = require('rollup-plugin-typescript2')
2 | const pkg = require('../package.json')
3 | const banner = `/*!
4 | * ${pkg.name} v${pkg.version}
5 | * (c) ${new Date().getFullYear()} @ioing
6 | * Released under the ${pkg.license} License.
7 | */`
8 |
9 | module.exports = {
10 | input: ['./src/launcher.ts'],
11 | output: {
12 | banner,
13 | dir: './app',
14 | format: 'es'
15 | },
16 | plugins: [
17 | typescript({
18 | tsconfigOverride: {
19 | compilerOptions: {
20 | rootDir: "./src",
21 | outDir: "./app/",
22 | declaration: true,
23 | declarationDir: './app/typings/',
24 | module: 'esnext',
25 | target: 'es6',
26 | sourceMap: true
27 | }
28 | },
29 | useTsconfigDeclarationDir: true
30 | })
31 | ],
32 | external: ['html2canvas', '@webcomponents/webcomponentsjs', 'web-animations-js/web-animations.min.js'],
33 | onwarn: function (warning) {
34 | if (warning.code === 'THIS_IS_UNDEFINED') {
35 | return
36 | }
37 | console.error(warning.message)
38 | }
39 | }
--------------------------------------------------------------------------------
/scripts/util.js:
--------------------------------------------------------------------------------
1 | const childProcess = require('child_process')
2 | function sh (shell) {
3 | return new Promise((resolve, reject) => {
4 | childProcess.exec(shell, (err, stdout, stderr) => {
5 | if (err) return reject(err)
6 | process.stdout.write(stdout)
7 | process.stderr.write(stderr)
8 | resolve()
9 | })
10 | })
11 | }
12 |
13 | exports.sh = sh
14 |
--------------------------------------------------------------------------------
/src/AppSwitcher/cssText.ts:
--------------------------------------------------------------------------------
1 | export const switcherCSSText = `
2 | box-sizing: border-box;
3 | position: fixed;
4 | top: 0px;
5 | right: 0px;
6 | bottom: 0px;
7 | left: 0px;
8 | z-index: 102;
9 | width: 100%;
10 | height: 100%;
11 | background: rgb(70 70 70 / 50%);
12 | opacity: 0;
13 | transform: translate3d(0, 0, 0);
14 | backface-visibility: hidden;
15 | transition: opacity .4s ease;
16 | backdrop-filter: blur(20px) saturate(180%);
17 | `
18 | export const snapWrapperCSSText = `
19 | display: grid;
20 | box-sizing: border-box;
21 | position: fixed;
22 | top: 0px;
23 | right: 0px;
24 | bottom: 0px;
25 | left: 0px;
26 | z-index: 2;
27 | width: 100%;
28 | height: 100%;
29 | padding: 20px 10px;
30 | padding-top: calc(20px + constant(safe-area-inset-top));
31 | padding-top: calc(20px + env(safe-area-inset-top));
32 | scroll-padding-top: calc(20px + constant(safe-area-inset-top));
33 | scroll-padding-top: calc(20px + env(safe-area-inset-top));
34 | overflow-y: hidden;
35 | scroll-behavior: smooth;
36 | transform: translate3d(0, 0, 0);
37 | backface-visibility: hidden;
38 | `
39 |
40 | export const snapWrapper2CSSText = `
41 | ${snapWrapperCSSText}
42 | grid-template-columns: 50% 50%;
43 | grid-template-rows: repeat(30, 40%);
44 | row-gap: 15px;
45 | `
46 |
47 | export const snapWrapper3CSSText = `
48 | ${snapWrapperCSSText}
49 | grid-template-columns: repeat(3, 33.33%);
50 | grid-template-rows: repeat(30, 50%);
51 | row-gap: 15px;
52 | `
53 |
54 | export const itemImgWrapperCSSText = `
55 | display: block;
56 | position: relative;
57 | width: 100%;
58 | height: 100%;
59 | border-radius: 16px;
60 | overflow: hidden;
61 | box-shadow: 0 3px 10px rgb(0 0 0 / 30%);
62 | `
63 |
64 | export const itemViewCSSText = `
65 | display: flex;
66 | flex-direction: column;
67 | box-sizing: border-box;
68 | scroll-snap-align: start;
69 | cursor: pointer;
70 | margin: 0 10px;
71 | transition: transform .4s ease;
72 | `
73 |
74 | export const itemImgCSSText = `
75 | display: block;
76 | width: 100%;
77 | height: 100%;
78 | border-radius: 16px;
79 | transform-origin: top left;
80 | transform: translate3d(0, 0, 0);
81 | backface-visibility: hidden;
82 | overflow: hidden;
83 | `
84 |
85 | export const itemImgCoverCSSText = `
86 | position: fixed;
87 | top: 0;
88 | left: 0;
89 | ${itemImgCSSText}
90 | `
91 |
92 | export const itemInfoCSSText = `
93 | display: flex;
94 | margin: 0 auto;
95 | width: 100%;
96 | justify-content: center;
97 | align-items: center;
98 | `
99 |
100 | export const itemTitleCSSText = `
101 | margin-top: 4px;
102 | font-size: 16px;
103 | color: #fff;
104 | overflow: hidden;
105 | white-space: nowrap;
106 | text-overflow: ellipsis;
107 | font-family: "SF Pro Text","Myriad Set Pro","SF Pro Icons","Apple Legacy Chevron","Helvetica","Arial",sans-serif;
108 | `
109 |
110 | export const itemCloseBtnCSSText = `
111 | display: grid;
112 | place-items: center;
113 | position: absolute;
114 | right: 4px;
115 | top: 4px;
116 | width: 22px;
117 | height: 22px;
118 | border-radius: 11px;
119 | background-color: #666;
120 | opacity: .7
121 | `
122 |
123 | const itemCloseBtnXShapeCSSText = `
124 | position: absolute;
125 | height: 2px;
126 | width: 60%;
127 | background: #fff;
128 | border-radius: 1px;
129 | `
130 |
131 | export const itemCloseBtnX1ShapeCSSText = `
132 | ${itemCloseBtnXShapeCSSText}
133 | transform: rotate(-45deg);
134 | `
135 | export const itemCloseBtnX2ShapeCSSText = `
136 | ${itemCloseBtnXShapeCSSText}
137 | transform: rotate(45deg);
138 | `
--------------------------------------------------------------------------------
/src/Applet/Base.ts:
--------------------------------------------------------------------------------
1 | import { EventProvider } from '../Event'
2 | import { Sandbox } from '../Sandbox'
3 | import manifestProcess from './manifestProcess'
4 | import { DefineApplet, Slide, Modality, AppletControls, AppletEvents, AppletAllConfig, AppletResources, AppletManifest, AppletAttachBehavior, Application, Applet } from '../types'
5 |
6 | class AppletBase extends EventProvider {
7 | public id: string
8 | public param = ''
9 | public application: Application
10 | public viewport?: HTMLElement
11 | public mountBehavior?: AppletAttachBehavior
12 | public controls?: AppletControls
13 | public contentView?: DefineApplet
14 | public contentSlot?: HTMLSlotElement
15 | public parentApplet?: Applet
16 | public sandbox: Sandbox | undefined
17 | public slide: Slide | null = null
18 | public modality: Modality | null = null
19 | public view: HTMLElement | HTMLPortalElement | HTMLIFrameElement | null = null
20 | public img: HTMLElement | null = null
21 | public snapshot: HTMLCanvasElement | null = null
22 | public snapshotTime = -1
23 | public visitTime = -1
24 | public model: AppletManifest
25 | public events: AppletEvents = {
26 | transformStart: () => undefined,
27 | transformEnd: () => undefined,
28 | boot: () => undefined,
29 | load: () => undefined,
30 | loadError: () => undefined,
31 | preload: () => undefined,
32 | destroy: () => undefined
33 | }
34 | public darkTask: Array<() => void> = []
35 | public createTime = Date.now()
36 | public transient = false
37 | public config: AppletAllConfig = {
38 | title: '',
39 | level: 0,
40 | source: {},
41 | prerender: [],
42 | apply: undefined,
43 | background: 'auto',
44 | free: false
45 | }
46 | public components: ((w: Window) => void)[] = []
47 | public resources: AppletResources = {
48 | script: [],
49 | image: [],
50 | worker: [],
51 | video: [],
52 | audio: [],
53 | font: [],
54 | style: []
55 | }
56 | get self() {
57 | return this.application.applets[this.id] || this
58 | }
59 | constructor(id: string, model: AppletManifest, application: Application) {
60 | super()
61 | this.id = id
62 | this.model = model
63 | this.application = application
64 |
65 | const { config, resources, events, components } = this.setDefaultConfig(model, id)
66 | Object.assign(this.config, config)
67 | Object.assign(this.resources, resources)
68 | Object.assign(this.events, events)
69 | if (components) {
70 | this.components = components
71 | }
72 | // preloadAnimation
73 | this.application.segue.preloadAnimation(this.self)
74 | }
75 | public setDefaultConfig = manifestProcess
76 | }
77 |
78 | export {
79 | AppletBase
80 | }
81 |
--------------------------------------------------------------------------------
/src/Applet/EventTarget.ts:
--------------------------------------------------------------------------------
1 | import { AppletPrefetch } from './Prefetch'
2 |
3 | class AppletEventTarget extends AppletPrefetch {
4 | public borderHolder: HTMLElement[] = []
5 | public attachEvent(): void {
6 | this.registerBorderTouch()
7 | this.bindOverscrollHistoryNavigation()
8 | this.registerTapStatusBarToScrollToTop()
9 | this.registerPullToRefresh()
10 | }
11 | private scrollToTop(): void {
12 | const scroller = this.getMainScroller()
13 | if (!scroller) return
14 | import('../Scroll').then(({ SmoothScroller }) => {
15 | const smoothScroller = new SmoothScroller(scroller)
16 | smoothScroller.scrollTo(0, 0)
17 | }).catch((e) => {
18 | console.warn(e)
19 | })
20 | }
21 |
22 | private registerPullToRefresh(): void {
23 | if (!this.config.pullToRefresh) return
24 | if (!this.application.pullRefreshHolder.holder) {
25 | this.application.on('pullToRefreshAvailable', () => {
26 | this.registerPullToRefresh()
27 | })
28 | return
29 | }
30 | const holder = this.application.pullRefreshHolder.holder
31 | const spinner = this.application.pullRefreshHolder.spinner
32 | const holdLayer = this.application.pullRefreshHolder.holdLayer
33 | const scroller = this.getMainScroller(this.config.pullToRefreshTargetScrollId)
34 | if (!scroller) return
35 | import('../pullToRefresh').then((PullRefresh) => {
36 | PullRefresh.registerPullDownEvent(this.self, scroller as HTMLElement, holder, spinner, holdLayer)
37 | }).catch((e) => {
38 | console.warn(e)
39 | })
40 | }
41 |
42 | private registerTapStatusBarToScrollToTop(): void {
43 | if (!this.config.tapStatusBarToScrollToTop) return
44 | const topHolder = this.borderHolder[0]
45 | topHolder.addEventListener('touchend', () => {
46 | this.scrollToTop()
47 | })
48 | }
49 |
50 | private getMainScroller(id: string | undefined = this.config.mainScrollId): HTMLElement | null | undefined {
51 | let scroller: HTMLElement | null | undefined
52 | if (this.viewType === 'shadow') {
53 | scroller = id ? (this.view?.shadowRoot?.getElementById(id) || document.getElementById(id)) : this.contentView
54 | } else if (this.sameOrigin) {
55 | const sandboxWindow = this.sandbox?.window
56 | scroller = id ? sandboxWindow?.document.getElementById(id) : sandboxWindow?.document.body
57 | }
58 | return scroller
59 | }
60 |
61 | private bindOverscrollHistoryNavigation(): void {
62 | const updateTriggerTime = (types: string[], event: TouchEvent) => {
63 | if (event.type !== 'touchmove') return
64 | if (types.includes('left') || types.includes('right') || types.includes('wipe')) {
65 | const timestamp = Date.now()
66 | const touchType = types.join(' ')
67 | this.application.overscrollHistoryNavigation.moment = timestamp
68 | this.application.overscrollHistoryNavigation.type = touchType
69 |
70 | // Continue to send the message up to the parent window.
71 | parent.postMessage({
72 | type: 'overscrollHistoryNavigationStatusChange',
73 | data: [timestamp, touchType]
74 | }, '*')
75 | }
76 | }
77 | this.on('touchBorder', updateTriggerTime)
78 | }
79 |
80 | private addBorderPanMoveHolder(viewport: HTMLElement): HTMLDivElement[] {
81 | const threshold = this.config.borderTouchSize ?? 15
82 | const topHolder = document.createElement('div')
83 | const rightHolder = document.createElement('div')
84 | const bottomHolder = document.createElement('div')
85 | const leftHolder = document.createElement('div')
86 | const mainHolder = document.createElement('div')
87 | const baseStyle = 'position: absolute; z-index: 3;'
88 | topHolder.style.cssText = `${baseStyle} top: 0; right: 0; left: 0; height: env(safe-area-inset-top); mini-height: ${threshold}px;`
89 | rightHolder.style.cssText = `${baseStyle} top: 0; right: 0; bottom: 0; z-index: 3; width: ${threshold}px;`
90 | bottomHolder.style.cssText = `${baseStyle} right: 0; bottom: 0; left: 0; z-index: 3; height: ${threshold}px;`
91 | leftHolder.style.cssText = `${baseStyle} top: 0; bottom: 0; left: 0; z-index: 3; width: ${threshold}px;`
92 | mainHolder.style.cssText = `display: none; ${baseStyle} top: 0; right: 0; bottom: 0; left: 0; z-index: 4;`
93 | viewport.appendChild(topHolder)
94 | viewport.appendChild(rightHolder)
95 | viewport.appendChild(bottomHolder)
96 | viewport.appendChild(leftHolder)
97 | viewport.appendChild(mainHolder)
98 |
99 | return this.borderHolder = [topHolder, rightHolder, bottomHolder, leftHolder, mainHolder]
100 | }
101 |
102 | private getTouchBorderType = (event: TouchEvent): string[] => {
103 | const viewport = this.viewport as HTMLElement
104 | const contentWidth = viewport.offsetWidth
105 | const contentHeight = viewport.offsetHeight
106 | const touches = event.touches[0]
107 | const x = touches.clientX
108 | const y = touches.clientY
109 | const types: string[] = []
110 | const threshold = this.config.borderTouchSize ?? 100
111 | if (x <= threshold) {
112 | types.push('left')
113 | } else if (x >= contentWidth - threshold) {
114 | types.push('right')
115 | }
116 | if (y <= threshold) {
117 | types.push('top')
118 | } else if (y >= contentHeight - threshold) {
119 | types.push('bottom')
120 | }
121 | return types
122 | }
123 |
124 | private registerBorderHolderTouch(): void {
125 | const [topHolder, rightHolder, bottomHolder, leftHolder, mainHolder] = this.addBorderPanMoveHolder(this.viewport as HTMLElement)
126 | const touchTypes: string[] = []
127 | const contentBinder = mainHolder.addEventListener
128 | const contentUnBinder = mainHolder.removeEventListener
129 | const trigger = (event: TouchEvent) => {
130 | this.trigger('touchBorder', touchTypes, event)
131 | }
132 | const bindPlaceholder = (event: TouchEvent): void => {
133 | touchTypes.length = 0
134 | if (event.target === topHolder) {
135 | touchTypes.push('top')
136 | } else if (event.target === rightHolder) {
137 | touchTypes.push('right')
138 | } else if (event.target === bottomHolder) {
139 | touchTypes.push('bottom')
140 | } else if (event.target === leftHolder) {
141 | touchTypes.push('left')
142 | }
143 | mainHolder.style.display = 'block'
144 | contentBinder('touchmove', trigger, true)
145 | contentBinder('touchend', trigger, true)
146 | contentBinder('touchcancel', trigger, true)
147 | }
148 | const unbindPlaceholder = (): void => {
149 | mainHolder.style.display = 'none'
150 | contentUnBinder('touchmove', trigger, true)
151 | contentUnBinder('touchend', trigger, true)
152 | contentUnBinder('touchcancel', trigger, true)
153 | }
154 | const binder = (holder: HTMLElement): void => {
155 | const holderBinder = holder.addEventListener
156 | holderBinder('touchstart', bindPlaceholder, true)
157 | holderBinder('touchmove', bindPlaceholder, true)
158 | holderBinder('touchcancel', unbindPlaceholder, true)
159 | holderBinder('touchend', unbindPlaceholder, true)
160 | }
161 | binder(topHolder)
162 | binder(rightHolder)
163 | binder(bottomHolder)
164 | binder(leftHolder)
165 | }
166 |
167 | private registerBorderTouch(): void {
168 | if (this.sandbox && !this.sameOrigin) return this.registerBorderHolderTouch()
169 | const target = this.sandbox?.window?.document || this.viewport as HTMLElement
170 | const touchTypes: string[] = []
171 | const trigger = ((event: TouchEvent) => {
172 | this.trigger('touchBorder', touchTypes, event)
173 | if (event.type === 'touchend' || event.type === 'touchcancel') {
174 | target.removeEventListener('touchmove', trigger, true)
175 | target.removeEventListener('touchend', trigger, true)
176 | target.removeEventListener('touchcancel', trigger, true)
177 | }
178 | }) as EventListener
179 | const listener = ((event: TouchEvent): void => {
180 | touchTypes.length = 0
181 | touchTypes.push(...this.getTouchBorderType(event))
182 | if (this.transforming) {
183 | touchTypes.push('wipe')
184 | }
185 | if (touchTypes.length > 0) {
186 | target.addEventListener('touchmove', trigger, true)
187 | target.addEventListener('touchend', trigger, true)
188 | target.addEventListener('touchcancel', trigger, true)
189 | }
190 | }) as EventListener
191 | target.addEventListener('touchstart', listener, true)
192 | }
193 | }
194 |
195 | export {
196 | AppletEventTarget
197 | }
198 |
--------------------------------------------------------------------------------
/src/Applet/LifeCycle.ts:
--------------------------------------------------------------------------------
1 | import { AppletState } from './State'
2 | import { Applet, AppletAttachBehavior } from '../types'
3 | import { setTimeout } from '../lib/util'
4 | class AppletLifeCycle extends AppletState {
5 | private mutationObserver!: MutationObserver
6 |
7 | public attach(element: HTMLElement, parentApplet?: Applet, mountBehavior?: AppletAttachBehavior): void {
8 | if (!element) return
9 | this.viewport = element
10 | this.parentApplet = parentApplet
11 | this.mountBehavior = mountBehavior
12 | }
13 |
14 | public removeContainer() {
15 | if (this.isPresetAppletsView) return
16 | this.contentView?.parentElement?.removeChild(this.contentView)
17 | this.viewport?.parentNode?.removeChild(this.viewport as HTMLElement)
18 | this.viewport = undefined
19 | this.delPresetView()
20 | }
21 |
22 | public clearContainer() {
23 | if (this.isPresetAppletsView) return
24 | this.contentView?.parentElement?.removeChild(this.contentView)
25 | if (this.viewport) {
26 | this.viewport.innerHTML = ''
27 | }
28 | this.delPresetView()
29 | }
30 |
31 | public delPresetView() {
32 | this.contentView = undefined
33 | this.application.delPresetAppletsView(this.id, true)
34 | }
35 |
36 | public setParam(param: string): Promise {
37 | if (this.param !== param) {
38 | this.param = param
39 | return this.destroy(true)
40 | }
41 | return Promise.resolve(true)
42 | }
43 |
44 | public show(): void {
45 | const viewType = this.viewType
46 | if (viewType === 'portal') {
47 | (this.view as HTMLPortalElement)?.activate()
48 | return
49 | }
50 | for (const task of this.darkTask) {
51 | task()
52 | }
53 | this.transforming = false
54 | this.darkTask = []
55 | this.visibility = true
56 | this.visitTime = Date.now()
57 | this.trigger('show')
58 | if (viewType === 'iframe') {
59 | this.triggerWindow('applet-show')
60 | this.injectWindowVisibilityState('visible')
61 | }
62 | this.subApplet?.show()
63 | }
64 |
65 | public hide(): void {
66 | this.transforming = false
67 | this.visibility = false
68 | this.trigger('hide')
69 | if (this.viewType === 'iframe') {
70 | this.triggerWindow('applet-hidden')
71 | this.injectWindowVisibilityState('hidden')
72 | }
73 | this.subApplet?.hide()
74 | }
75 |
76 | public willShow(): void {
77 | this.transforming = true
78 | this.trigger('willShow')
79 | if (this.viewType === 'iframe') {
80 | this.triggerWindow('applet-will-show')
81 | this.injectWindowVisibilityState('willVisible')
82 | }
83 | this.subApplet?.willShow()
84 | }
85 |
86 | public willHide(): void {
87 | this.transforming = true
88 | this.trigger('willHide')
89 | if (this.viewType === 'iframe') {
90 | this.triggerWindow('applet-will-hide')
91 | this.injectWindowVisibilityState('willHidden')
92 | }
93 | this.subApplet?.willHide()
94 | }
95 |
96 | public willSegueShow(): void {
97 | this.trigger('willSegueShow')
98 | }
99 |
100 | public willSegueHide(): void {
101 | this.trigger('willSegueHide')
102 | }
103 |
104 | public destroy(reserve = false, referenceChecking = true): Promise {
105 | if (referenceChecking) {
106 | [...this.allSubAppletIds, ...(this.parentApplet?.allSubAppletIds ?? [])].forEach((subAppletId) => this.application.applets[subAppletId]?.destroy(false, false))
107 | }
108 | return new Promise((resolve) => {
109 | if (!this.view) return resolve(true)
110 | if (this.isPresetAppletsView) return resolve(false)
111 | if (this.rel === 'frameworks' || this.rel === 'system') return resolve(false)
112 | if (this.application.segue?.id === this.id && reserve === false) return resolve(false)
113 | if (this.viewType === 'iframe') this.unload()
114 | if (reserve === false) this.removeContainer()
115 | else this.clearContainer()
116 | this.resume()
117 | this.events?.destroy(this.self)
118 | this.application.removeEventGroup(this.id)
119 | this.trigger('destroy')
120 | resolve(true)
121 | })
122 | }
123 |
124 | public observer(change: (record: MutationRecord[]) => void): MutationObserver | undefined {
125 | const target = this.sandbox ? this.sandbox.document?.documentElement : this.view
126 | if (!target) return
127 | const observer = new MutationObserver((record: MutationRecord[]) => {
128 | change(record)
129 | })
130 | observer.observe(target, {
131 | subtree: true,
132 | attributes: true,
133 | childList: true,
134 | characterData: true,
135 | attributeOldValue: true,
136 | characterDataOldValue: true
137 | })
138 | return observer
139 | }
140 |
141 | public collectAllElements(root: Element): Element[] {
142 | const allElements: Array = []
143 | const findAllElements = function (nodes: NodeListOf) {
144 | for (let i = 0; i < nodes.length; i++) {
145 | const el = nodes[i]
146 | allElements.push(el)
147 | if (el.shadowRoot) {
148 | findAllElements(el.shadowRoot.querySelectorAll('*'))
149 | }
150 | }
151 | }
152 | if (root.shadowRoot) {
153 | findAllElements(root.shadowRoot.querySelectorAll('*'))
154 | }
155 | findAllElements(root.querySelectorAll('*'))
156 |
157 | return allElements
158 | }
159 |
160 | public autoMediaController(allElements: Array): Promise {
161 | return new Promise((resolve, reject) => {
162 | try {
163 | if (this.viewType !== 'iframe') return resolve()
164 | if (this.sandbox === undefined) return resolve()
165 | const videoAndAudioList = allElements.filter(el => el.tagName === 'video' || el.tagName === 'audio')
166 | for (const index in videoAndAudioList) {
167 | const videoAndAudio = videoAndAudioList[index] as HTMLVideoElement | HTMLAudioElement
168 | if (!videoAndAudio?.paused) {
169 | videoAndAudio.pause()
170 | this.darkTask.push(() => {
171 | videoAndAudio.play()
172 | })
173 | }
174 | }
175 | } catch (error) {
176 | reject()
177 | }
178 | })
179 | }
180 |
181 | public destructiveTags(allElements: Array): boolean {
182 | const blockTags = ['object', 'embed', 'applet', 'iframe']
183 | const matchTags = allElements.filter(el => blockTags.includes(el.tagName))
184 | return matchTags.length ? true : false
185 | }
186 |
187 | public guarding(): Promise {
188 | return new Promise((resolve) => {
189 | if (this.rel !== 'applet') return resolve(true)
190 | if (this.sandbox === undefined) return resolve(true)
191 | if (this.config.background === true) return resolve(true)
192 | if (this.config.background === false) return resolve(false)
193 | if (this.sameOrigin === false) return resolve(false)
194 | const view = this.view as HTMLIFrameElement
195 | try {
196 | const contentDocumentElement = view.contentDocument?.documentElement
197 | if (!contentDocumentElement) return resolve(true)
198 | const allElements = this.collectAllElements(contentDocumentElement)
199 | if (this.config.mediaGuard) this.autoMediaController(allElements).catch(resolve)
200 | if (this.destructiveTags(allElements)) return resolve(false)
201 | if (this.config.observerGuard) {
202 | const counter = { times: 0 }
203 | const observer = this.observer(() => {
204 | counter.times++
205 | if (counter.times > 10000) {
206 | resolve(false)
207 | this.mutationObserver.disconnect()
208 | }
209 | })
210 | if (!observer) return
211 | this.mutationObserver = observer
212 | setTimeout(() => {
213 | if (counter.times > 10) resolve(false)
214 | }, 3000)
215 | }
216 | } catch (error) {
217 | resolve(true)
218 | }
219 | })
220 | }
221 |
222 | public cancelGuard(): void {
223 | this.mutationObserver?.disconnect()
224 | }
225 |
226 | public unload(sandbox = this.sandbox?.sandbox): Promise {
227 | return new Promise((resolve) => {
228 | this.cancelGuard()
229 | if (!sandbox) return resolve()
230 | if (sandbox === this.sandbox?.sandbox) this.saveMirroring()
231 | /*
232 | * IFrame Memory Leak when removing IFrames
233 | */
234 | try {
235 | if (sandbox.contentWindow) {
236 | sandbox.contentWindow.onunload = null
237 | }
238 | sandbox.style.display = 'none'
239 | sandbox.src = 'about:blank'
240 | const contentWindow = sandbox.contentWindow?.window
241 | const contentDocument = sandbox.contentDocument
242 | contentWindow?.location.reload()
243 | contentDocument?.open()
244 | contentDocument?.write('')
245 | contentDocument?.close()
246 | } catch (error) {
247 | //
248 | }
249 | sandbox.parentNode?.removeChild(sandbox)
250 | resolve()
251 | })
252 | }
253 | }
254 |
255 | export {
256 | AppletLifeCycle
257 | }
258 |
--------------------------------------------------------------------------------
/src/Applet/Prefetch.ts:
--------------------------------------------------------------------------------
1 | import { AppletLifeCycle } from './LifeCycle'
2 | import { Sandbox } from '../Sandbox'
3 | import { requestIdleCallback } from '../lib/util'
4 |
5 | class AppletPrefetch extends AppletLifeCycle {
6 | public dependenciesLoad(uri?: string, type: 'src' | 'source' = 'src'): Promise {
7 | return new Promise((resolve, reject) => {
8 | const head = document.head
9 | const sandbox = new Sandbox(uri, '', type)
10 | sandbox.setOnLoad((e) => {
11 | // wait async script
12 | setTimeout(() => {
13 | sandbox.exit()
14 | }, 2000)
15 | resolve(e)
16 | })
17 | sandbox.setOnError((e) => {
18 | sandbox.exit()
19 | reject(e)
20 | })
21 | sandbox.enter(head)
22 | })
23 | }
24 |
25 | public async preload(): Promise {
26 | return this.dependenciesLoad(this.uri || await this.source, this.uri ? 'src' : 'source').then(() => {
27 | this.status.preload = true
28 | this.events?.preload(this.self)
29 | this.trigger('preload')
30 | })
31 | }
32 |
33 | public prefetch(): Promise {
34 | return new Promise((resolve, reject) => {
35 | Promise.all([
36 | this.prefetchStatic(this.resources.script, 'script'),
37 | this.prefetchStatic(this.resources.image, 'image'),
38 | this.prefetchStatic(this.resources.worker, 'worker'),
39 | this.prefetchStatic(this.resources.video, 'video'),
40 | this.prefetchStatic(this.resources.audio, 'audio'),
41 | this.prefetchStatic(this.resources.font, 'font'),
42 | this.prefetchStatic(this.resources.style, 'style'),
43 | this.prefetchStatic(this.resources.html, 'document')
44 | ]).then(() => {
45 | this.status.prefetch = true
46 | this.trigger('prefetch')
47 | resolve(true)
48 | }).catch(reject)
49 | })
50 | }
51 |
52 | public prefetchStatic(list: string[] = [], as = 'script'): Promise<(string | undefined | Event)[]> {
53 | return new Promise((resolve, reject) => {
54 | Promise.all([].concat(list as []).map(url => this.prelink(url, 'preload', as))).then(resolve).catch(reject)
55 | })
56 | }
57 |
58 | public prelink(url: string, rel: 'prefetch' | 'prerender' | 'preload' = 'preload', as = 'worker | video | audio | font | script | style | image | document'): Promise {
59 | if (!url) return Promise.resolve(undefined)
60 | return new Promise((resolve, reject) => {
61 | const load = () => {
62 | const link = document.createElement('link')
63 | link.rel = rel
64 | link.href = url
65 | link.as = as
66 | link.onload = resolve
67 | link.onerror = reject
68 | if (rel === 'preload' && as === 'document') {
69 | this.dependenciesLoad(url)
70 | }
71 | if (!link.relList?.supports(rel)) {
72 | return resolve(undefined)
73 | }
74 | document.getElementsByTagName('head')[0].appendChild(link)
75 | resolve(undefined)
76 | }
77 | requestIdleCallback(load, { timeout: 15000 })
78 | })
79 | }
80 |
81 | public async prerender(): Promise {
82 | if (this.status.preload || this.status.prerender) {
83 | return Promise.resolve()
84 | }
85 | await this.preload()
86 | await this.prefetch()
87 |
88 | return Promise.resolve()
89 | }
90 | }
91 |
92 | export {
93 | AppletPrefetch
94 | }
95 |
--------------------------------------------------------------------------------
/src/Applet/State.ts:
--------------------------------------------------------------------------------
1 | import { AppletBase } from './Base'
2 | import getIOSVersion from '../lib/util/getIOSVersion'
3 | import { Applet, AppletStatus, SegueActionOrigin, SwipeModelType } from '../types'
4 |
5 | class AppletState extends AppletBase {
6 | public visibility = false
7 | public transforming = false
8 | public segueActionOrigin?: SegueActionOrigin
9 | public viewLevel = 0
10 | public status: AppletStatus = {
11 | preload: false,
12 | prefetch: false,
13 | prerender: false,
14 | refreshing: false,
15 | requestRefresh: false
16 | }
17 | get swipeModel(): SwipeModelType {
18 | // Use native performance optimizations only for iOS.
19 | return this.application.config.swipeModel === 'default' ? ((("ontouchend" in document) && (getIOSVersion()?.[0] ?? 0) >= 9) ? 'default' : false) : true
20 | }
21 | get sameOrigin(): boolean {
22 | if (!this.uri) {
23 | if (this.config.sandbox === undefined) return true
24 | if (this.config.sandbox.includes('allow-same-origin')) return true
25 | return false
26 | }
27 | const link = new URL(
28 | this.uri,
29 | window.location.toString()
30 | )
31 | const isSameOrigin = link.host === window.location.host
32 | return isSameOrigin
33 | }
34 | get level(): number {
35 | return this.config.level ?? 0
36 | }
37 | public setLevel(index: number) {
38 | this.viewLevel = index
39 | }
40 | get rel(): 'system' | 'frameworks' | 'applet' {
41 | if (this.id === 'system') return 'system'
42 | if (this.id === 'frameworks') return 'frameworks'
43 | return 'applet'
44 | }
45 | get uri(): string {
46 | return this.config?.source?.src || ''
47 | }
48 | get source(): string | Promise {
49 | const html = this.config?.source?.html || ''
50 | return typeof html === 'string' ? html : html()
51 | }
52 | get hasSource(): boolean {
53 | return (this.uri || this.config?.source?.html) ? true : false
54 | }
55 | get noSource(): boolean {
56 | return !this.hasSource && !this.config.render
57 | }
58 | get viewType(): 'portal' | 'iframe' | 'shadow' {
59 | if (this.rel !== 'applet') return 'shadow'
60 | if (this.hasSource) {
61 | if (this.config?.portal && this.sameOrigin) {
62 | return 'portal'
63 | }
64 | return 'iframe'
65 | }
66 | return 'shadow'
67 | }
68 | get color(): string {
69 | if (this.rel !== 'applet') return 'transparent'
70 | const application = this.application
71 | const isDarkModel = application.properties.darkTheme
72 | const color = `${this.config.color || (isDarkModel ? '#000' : '#fff')}`
73 | const inherit = this.id !== 'frameworks' && color === 'inherit'
74 | return inherit ? (application.applets.frameworks.config.color || color) : color
75 | }
76 | get expired(): boolean {
77 | if (!this.view) return false
78 | if (this.application.prevActivityApplet?.config.modality) {
79 | return false
80 | }
81 | if (this.status.requestRefresh || Date.now() - this.createTime >= (this.config.timeout ?? 60000000)) {
82 | return true
83 | }
84 | return false
85 | }
86 | get isModality(): boolean {
87 | return !!this.config.modality
88 | }
89 | get isFullscreen(): boolean {
90 | return !this.isModality
91 | }
92 | get isPresetAppletsView(): boolean {
93 | return this.application.checkIsPresetAppletsView(this.id)
94 | }
95 | get useControls(): boolean {
96 | return this.config.disableSwipeModel !== true && this.swipeModel && this.rel === 'applet' && this.isFullscreen
97 | }
98 | get noShadowDom(): boolean {
99 | if (this.viewType === 'shadow') {
100 | if (this.config.noShadowDom) {
101 | return true
102 | }
103 | return false
104 | }
105 | return false
106 | }
107 | get mirroringString(): string {
108 | try {
109 | return localStorage.getItem('__MODULE_IMG__' + this.id) || ''
110 | } catch (error) {
111 | return ''
112 | }
113 | }
114 | get contentWindow(): Window {
115 | return this.sandbox?.window?.window ?? window
116 | }
117 | get contentDocument(): HTMLElement | Document | undefined {
118 | if (!this.sameOrigin) return
119 | return this.viewType === 'shadow' ? this.viewport : this.sandbox?.document
120 | }
121 | get mirroringHTML(): string {
122 | if (this.viewType === 'shadow') {
123 | return this.view?.outerHTML ?? ''
124 | }
125 | if (!this.sameOrigin) return ''
126 | const sandbox = this.sandbox?.sandbox as HTMLIFrameElement
127 | const blockTags = ['script', 'template']
128 | try {
129 | const contentDocument = sandbox.contentDocument
130 | if (!contentDocument) return ''
131 | const allElements = contentDocument.querySelectorAll('*')
132 | const allElementsList: Array = []
133 | allElements.forEach((element) => {
134 | const tagName = element.tagName
135 | if (blockTags.includes(tagName)) return
136 | if (tagName.indexOf('-') !== -1) {
137 | (element as HTMLElement).style.visibility = 'hidden'
138 | }
139 | allElementsList.push(element.outerHTML)
140 | })
141 | return allElementsList.join('\n')
142 | } catch (error) {
143 | return ''
144 | }
145 | }
146 | get subApplet(): Applet | undefined {
147 | if (this.slide) {
148 | return this.application.applets[this.slide.activeId]
149 | }
150 | return
151 | }
152 | get allSubAppletIds(): string[] {
153 | const applets = []
154 | if (this.slide) {
155 | for (const slideItem of this.slide.slideViewApplets) {
156 | applets.push(slideItem.id)
157 | }
158 | }
159 | return applets
160 | }
161 | protected resume() {
162 | this.status = {
163 | preload: false,
164 | prefetch: false,
165 | prerender: false,
166 | refreshing: false,
167 | requestRefresh: false
168 | }
169 | this.darkTask = []
170 | this.view = null
171 | this.img = null
172 | if (this.slide) {
173 | this.slide = null
174 | }
175 | if (this.modality) {
176 | this.modality = null
177 | }
178 | }
179 | public saveMirroring(): Promise {
180 | if (!this.config.useMirroring || !this.sameOrigin) {
181 | return Promise.resolve(false)
182 | }
183 |
184 | const key = '__MODULE_IMG__' + this.id
185 | const documentHTML = this.mirroringHTML ?? ''
186 |
187 | try {
188 | localStorage.setItem(key, documentHTML)
189 | return Promise.resolve(true)
190 | } catch {
191 | localStorage.clear()
192 | localStorage.setItem(key, documentHTML)
193 | return Promise.resolve(false)
194 | }
195 | }
196 | public setActionOrigin(origin?: SegueActionOrigin): void {
197 | if (!origin) return
198 | this.segueActionOrigin = origin
199 | }
200 | public getActionOrigin(): SegueActionOrigin | undefined {
201 | return this.segueActionOrigin
202 | }
203 | public injectWindowVisibilityState(visibility: Window['appletVisibilityState']): void {
204 | if (this.sameOrigin) {
205 | const contentWindow = this.contentWindow
206 | if (contentWindow) {
207 | contentWindow.appletVisibilityState = visibility
208 | }
209 | }
210 | }
211 | public triggerWindow(type: string): void {
212 | this.contentWindow?.postMessage({
213 | type,
214 | appletId: this.id,
215 | historyDirection: this.application.segue.historyDirection
216 | }, '*')
217 | }
218 | }
219 |
220 | export {
221 | AppletState
222 | }
223 |
--------------------------------------------------------------------------------
/src/Applet/captureShot/index.ts:
--------------------------------------------------------------------------------
1 | import { Applet } from '../../types'
2 | import html2canvas from 'html2canvas'
3 |
4 | export const capture = async (applet: Applet) => {
5 | const appletView: HTMLElement = applet.sandbox && applet.sameOrigin
6 | ? applet.contentWindow.document.documentElement
7 | : applet.view as HTMLElement
8 | const viewport = applet.viewport
9 | if (!viewport) {
10 | throw ('Capture Shot: The applet has not been initialized!')
11 | }
12 | const viewportWidth = viewport.offsetWidth
13 | const viewportHeight = viewport.offsetHeight
14 |
15 | /**
16 | * If it is a shadow view type, take a screenshot by cloning.
17 | * The aim is to reduce complexity and prevent interference.
18 | * HTML2canvas will cause the slot to be lost.
19 | */
20 | const { application } = applet
21 | const ignoreAttrName = 'data-html2canvas-ignore'
22 | let cloneAppletView: HTMLElement | undefined
23 | let shotWrapper: HTMLElement | undefined
24 | if (applet.view === appletView) {
25 | cloneAppletView = appletView.cloneNode(true) as HTMLElement
26 | application.appletsSpace.setAttribute(ignoreAttrName, 'true')
27 | shotWrapper = document.createElement('div')
28 | shotWrapper.style.opacity = '0'
29 | shotWrapper.style.contain = 'strict'
30 | shotWrapper.appendChild(cloneAppletView)
31 | document.body.appendChild(shotWrapper)
32 | }
33 | const color = /\(/.exec(applet.color) ? (applet.view ? getComputedStyle(applet.view).backgroundColor : 'transparent') : applet.color
34 | const canvas = await html2canvas(cloneAppletView || appletView, {
35 | backgroundColor: color,
36 | useCORS: true,
37 | width: viewportWidth,
38 | height: viewportHeight,
39 | windowWidth: viewportWidth,
40 | windowHeight: viewportHeight,
41 | x: appletView.scrollLeft,
42 | y: appletView.scrollTop,
43 | removeContainer: true,
44 | imageTimeout: 2000
45 | })
46 | canvas.style.cssText = `
47 | width: 100% !important;
48 | height: auto !important;
49 | transition: all .4s ease;
50 | `
51 | /**
52 | * Clear clone node.
53 | */
54 | if (shotWrapper) {
55 | document.body.removeChild(shotWrapper)
56 | application.appletsSpace.removeAttribute(ignoreAttrName)
57 | }
58 |
59 | return canvas
60 | }
--------------------------------------------------------------------------------
/src/Applet/index.ts:
--------------------------------------------------------------------------------
1 | import { AppletView } from './View'
2 | import { AppletManifest, Application } from '../types'
3 |
4 | class Applet extends AppletView {
5 | constructor(id: string, model: AppletManifest, application: Application) {
6 | super(id, model, application)
7 | this.events.boot(this.self)
8 | }
9 | }
10 |
11 | export {
12 | Applet
13 | }
14 |
--------------------------------------------------------------------------------
/src/Applet/inject/cssVar.ts:
--------------------------------------------------------------------------------
1 |
2 | import { testHasScrolling } from '../../lib/util'
3 | import { Applet, ApplicationSafeAreaValue, GlobalCSSVariables } from '../../types'
4 |
5 | export default (appletWindow: Window, applet: Applet): void => {
6 | const { id, application } = applet
7 | const globalCSSVariablesConfig = application.config.globalCSSVariables
8 | const globalCSSVariables = typeof globalCSSVariablesConfig === 'function' ? globalCSSVariablesConfig() : globalCSSVariablesConfig
9 | const docStyle = appletWindow.document.documentElement.style
10 | const setGlobalCSSVariables = (variables: GlobalCSSVariables): void => {
11 | // clear snapshot
12 | applet.snapshot = null
13 | for (const key in variables) {
14 | docStyle.setProperty(key, variables[key])
15 | }
16 | }
17 | const setCSSSafeAreaValue = (data: ApplicationSafeAreaValue): void => {
18 | setGlobalCSSVariables({
19 | '--application-safe-area-top': data[0] ?? data,
20 | '--application-safe-area-right': data[1] ?? data,
21 | '--application-safe-area-bottom': data[2] ?? data[0] ?? data,
22 | '--application-safe-area-left': data[3] ?? data[1] ?? data
23 | })
24 | }
25 | const safeAreaConfig = application.config.safeArea
26 | const safeArea = typeof safeAreaConfig === 'function' ? safeAreaConfig() : safeAreaConfig
27 | if (safeArea) {
28 | setCSSSafeAreaValue(safeArea)
29 | }
30 | if (globalCSSVariables) {
31 | setGlobalCSSVariables(globalCSSVariables)
32 | }
33 | const safeAreaChange = (data: ApplicationSafeAreaValue): void => {
34 | setCSSSafeAreaValue(data)
35 | }
36 | const globalCSSVariablesChange = (data: GlobalCSSVariables): void => {
37 | setGlobalCSSVariables(data)
38 | }
39 | /**
40 | * Obsolete
41 | * ------------- start -------------
42 | */
43 | // ios < 12.55 bug
44 | if (testHasScrolling() === false) {
45 | docStyle.cssText += 'width: 100vw; height: 100vh; overflow: auto; -webkit-overflow-scrolling: touch;'
46 | }
47 | /**
48 | * Obsolete
49 | * ------------- end -------------
50 | */
51 | application.on('safeAreaChange', safeAreaChange, id)
52 | application.on('globalCSSVariablesChange', globalCSSVariablesChange, id)
53 | }
54 |
--------------------------------------------------------------------------------
/src/Applet/inject/index.ts:
--------------------------------------------------------------------------------
1 | import openWindow from './openWindow'
2 | import { smartSetTimeout } from './smartSetTimeout'
3 | import tapHighlight from './tapHighlight'
4 | import cssVar from './cssVar'
5 | import { Applet } from '../../types'
6 |
7 | export const injectContext = (appletWindow: Window, applet: Applet): void => {
8 | const { config } = applet
9 | const apply = Array.from(new Set(config.apply))
10 | for (const item of apply) {
11 | switch (item) {
12 | case 'smart-setTimeout':
13 | smartSetTimeout(appletWindow)
14 | break
15 | default:
16 | break
17 | }
18 | }
19 | if (typeof config.inject === 'function') {
20 | try {
21 | config.inject(appletWindow, applet)
22 | } catch (error) {
23 | console.error('config > inject:', error)
24 | }
25 | }
26 | }
27 |
28 | export const injectDocument = (appletWindow: Window, applet: Applet): void => {
29 | const { config, application } = applet
30 | const apply = Array.from(new Set(config.apply))
31 | const param = config.applyOptions || {}
32 | for (const item of apply) {
33 | switch (item) {
34 | case 'proxy-link':
35 | openWindow(appletWindow, application)
36 | break
37 | case 'tap-highlight':
38 | tapHighlight(appletWindow, (param[item]?.selector ?? 'use-tap-highlight') as string)
39 | break
40 | default:
41 | break
42 | }
43 | }
44 | if (typeof config.injectToDocument === 'function') {
45 | try {
46 | config.injectToDocument(appletWindow, applet)
47 | } catch (error) {
48 | console.error('config > inject:', error)
49 | }
50 | }
51 | if (applet.components) {
52 | for (const mountComponent of applet.components) {
53 | mountComponent(appletWindow)
54 | }
55 | }
56 | cssVar(appletWindow, applet)
57 | }
58 |
--------------------------------------------------------------------------------
/src/Applet/inject/openWindow.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '../../types'
2 |
3 | interface ObsoleteEvent extends TouchEvent {
4 | path: Array
5 | }
6 |
7 | export default (appletWindow: Window, application: Application): void => {
8 | const realOpen = appletWindow.open
9 | const blockClick = (event: MouseEvent | TouchEvent): boolean => {
10 | if (event instanceof CustomEvent && event.detail instanceof Event) {
11 | event = event.detail as MouseEvent
12 | }
13 | const getTouches = (target: HTMLElement) => {
14 | return {
15 | x: (event as TouchEvent).changedTouches?.[0]?.pageX || (event as MouseEvent).x,
16 | y: (event as TouchEvent).changedTouches?.[0]?.pageY || (event as MouseEvent).y,
17 | target
18 | }
19 | }
20 | const getProps = (target: HTMLElement) => {
21 | return {
22 | title: target.getAttribute('title') || '',
23 | preset: target.getAttribute('preset-effect') || 'slide',
24 | cloneAs: target.getAttribute('clone-as') || undefined
25 | }
26 | }
27 | const path = (event as ObsoleteEvent).path || event.composedPath?.() || []
28 | path.splice(-3)
29 |
30 | for (const el of path) {
31 | const toApplet = el.getAttribute?.('to-applet')
32 | if (toApplet) {
33 | const { cloneAs, title, preset } = getProps(el)
34 | const [id, param] = toApplet.split('?')
35 | if (cloneAs) {
36 | application.get(id).then((applet) => {
37 | const appletManifest = applet.model
38 | Object.assign(appletManifest.config, {
39 | title,
40 | animation: preset,
41 | level: application.activityLevel
42 | })
43 | application.add(cloneAs, appletManifest)
44 | application.to(cloneAs, '?' + param, undefined, getTouches(el))
45 | })
46 | break
47 | }
48 | application.to(id, '?' + param, undefined, getTouches(el))
49 | break
50 | } else if (el.tagName === 'A') {
51 | const anchor = el as HTMLAnchorElement
52 | const href = anchor.href || String(anchor)
53 | if (href) {
54 | event.stopPropagation()
55 | event.preventDefault()
56 | if (anchor.target === '_parent' || anchor.target === '_top') {
57 | realOpen(href)
58 | return false
59 | }
60 | const { cloneAs, title, preset } = getProps(el)
61 | application.pushWindow(href, title, preset, cloneAs, getTouches(anchor)).catch(() => {
62 | realOpen(href)
63 | })
64 | }
65 | }
66 | }
67 | return false
68 | }
69 | appletWindow.document.addEventListener('click', blockClick, true)
70 | appletWindow.open = (url?: string | URL | undefined, target?: string | undefined, features?: string | undefined): Window | null => {
71 | if (typeof url === 'string' && (!target || target.indexOf('_') === 0) && !features) {
72 | application.pushWindow(url, '').catch(() => {
73 | realOpen(url)
74 | })
75 | } else {
76 | return realOpen(url, target, features)
77 | }
78 | return null
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Applet/inject/smartSetTimeout.ts:
--------------------------------------------------------------------------------
1 | interface TimerLog {
2 | [key: number]: boolean
3 | }
4 |
5 | interface TimerState {
6 | timerLog: TimerLog
7 | timerId: number
8 | }
9 |
10 | interface IntervalLog {
11 | [key: number]: IntervalState
12 | }
13 |
14 | interface IntervalState {
15 | timerId: number
16 | intervalId: number
17 | }
18 |
19 | const getTimerHandler = (handler: TimerHandler, appletWindow: Window, timerState: TimerState, stretch?: boolean) => {
20 | return () => {
21 | const run = () => {
22 | if (timerState.timerLog[timerState.timerId] === undefined) return
23 | if (typeof handler === 'function') {
24 | handler()
25 | } else if (typeof handler === 'string') {
26 | const evalHandle = new appletWindow.Function(`return ${handler}`)
27 | evalHandle()
28 | }
29 | delete timerState.timerLog[timerState.timerId]
30 | }
31 | // first render
32 | if (appletWindow.document.readyState !== 'complete') {
33 | run()
34 | return
35 | }
36 | // global scope
37 | if (appletWindow === window) {
38 | if (window.applicationActiveState !== 'frozen') {
39 | run()
40 | } else if (stretch) {
41 | window.addEventListener('message', (event: MessageEvent) => {
42 | if (event.data?.type === 'application-active') run()
43 | })
44 | }
45 | return
46 | }
47 | // applet scope
48 | if (appletWindow.appletVisibilityState === 'visible') {
49 | run()
50 | } else if (stretch) {
51 | appletWindow.addEventListener('message', (event: MessageEvent) => {
52 | if (event.data?.type === 'applet-show') run()
53 | })
54 | }
55 | }
56 | }
57 | export const smartSetTimeout = (appletWindow: Window): void => {
58 | const realSetTimeout = appletWindow.setTimeout
59 | const realClearTimeout = appletWindow.clearTimeout
60 | const realSetInterval = appletWindow.setInterval
61 | const realClearInterval = appletWindow.clearInterval
62 |
63 | const timerLog: TimerLog = {}
64 | appletWindow.setBackgroundTimeout = realSetTimeout
65 | appletWindow.setTimeout = (handler: TimerHandler, timeout?: number | undefined, ...args: unknown[]) => {
66 | const timerState = {
67 | timerLog,
68 | timerId: -1
69 | }
70 | const fn = getTimerHandler(handler, appletWindow, timerState, true)
71 | const intervalId = realSetTimeout(fn, timeout, ...args)
72 | timerLog[intervalId] = true
73 | timerState.timerId = intervalId
74 | return intervalId
75 | }
76 | appletWindow.clearTimeout = (...args) => {
77 | const timeoutID = args[0]
78 | if (timeoutID) {
79 | delete timerLog[timeoutID]
80 | }
81 | return realClearTimeout(...args)
82 | }
83 | appletWindow.setTimeout.toString = () => 'setTimeout() { [native code] }'
84 | appletWindow.clearTimeout.toString = () => 'clearTimeout() { [native code] }'
85 |
86 | // smartSetInterval
87 | const intervalLog: IntervalLog = {}
88 | appletWindow.setBackgroundInterval = realSetInterval
89 | appletWindow.setInterval = (handler: TimerHandler, timeout?: number | undefined, ...args: unknown[]) => {
90 | const timerState: IntervalState = {
91 | timerId: -1,
92 | intervalId: -1
93 | }
94 | const nextHandler = () => {
95 | if (timerState.timerId !== -1 && !intervalLog[timerState.intervalId]) return
96 | if (typeof handler === 'function') {
97 | handler()
98 | } else if (typeof handler === 'string') {
99 | const evalHandle = new appletWindow.Function(`return ${handler}`)
100 | evalHandle()
101 | }
102 | timerState.timerId = appletWindow.setTimeout(nextHandler, timeout, ...args)
103 | }
104 | const intervalId = timerState.intervalId = timerState.timerId = appletWindow.setTimeout(nextHandler, timeout, ...args)
105 | intervalLog[intervalId] = timerState
106 |
107 | return intervalId
108 | }
109 | appletWindow.clearInterval = (...args) => {
110 | const timeoutID = args[0]
111 | if (!timeoutID) return
112 | realClearInterval(timeoutID)
113 | const timerState = intervalLog[timeoutID]
114 | if (timerState) {
115 | appletWindow.clearTimeout(timerState.timerId)
116 | delete intervalLog[timerState.intervalId]
117 | }
118 | }
119 | appletWindow.setInterval.toString = () => 'setInterval() { [native code] }'
120 | appletWindow.clearInterval.toString = () => 'clearInterval() { [native code] }'
121 | }
122 |
--------------------------------------------------------------------------------
/src/Applet/inject/tapHighlight.ts:
--------------------------------------------------------------------------------
1 | import { setTimeout } from '../../lib/util'
2 |
3 | interface TouchActive {
4 | element: null | HTMLElement
5 | oldFilterStyle: string
6 | oldTransitionStyle: string
7 | waitingToAddTimeId: number
8 | }
9 |
10 | interface ObsoleteTouchEvent extends TouchEvent {
11 | path: Array
12 | }
13 |
14 | export default (appletWindow: Window, capture?: string | string[]): void => {
15 | const touchActive: TouchActive = {
16 | element: null,
17 | oldFilterStyle: '',
18 | oldTransitionStyle: '',
19 | waitingToAddTimeId: -1
20 | }
21 | const addHighlight = (event: TouchEvent): void => {
22 | const captureList = capture ? typeof capture === 'string' ? capture.split(' ') : capture : null
23 | const path = (event as ObsoleteTouchEvent).path || event.composedPath?.() || []
24 | path.splice(-3)
25 | const anchor: HTMLElement | null = (() => {
26 | for (const el of path) {
27 | if (el.tagName === 'A') return el
28 | if (!el.children?.length) continue
29 | if (captureList) {
30 | for (const attr of captureList) {
31 | if (attr.indexOf('#') === 0 && '#' + el.id === attr) return el
32 | if (attr.indexOf('.') === 0 && el.classList.length) {
33 | if (Object.values(el.classList).includes(attr.slice(1))) return el
34 | }
35 | if (el.getAttribute?.(attr)) return el
36 | }
37 | return null
38 | }
39 | }
40 | const target = (path[0]?.children?.length ? path[0] : path[1]) || event.target
41 | if (['DIV', 'A', 'IMG'].includes(target.tagName)) return target
42 | return null
43 | })()
44 | if (!anchor) return
45 | if (touchActive.element === anchor) return
46 | if (touchActive.element) {
47 | return cancelHighlight()
48 | }
49 | touchActive.element = anchor
50 | touchActive.oldFilterStyle = anchor.style.filter
51 | touchActive.oldTransitionStyle = anchor.style.transition
52 | // Prevent multiple types of click effects from overlaying
53 | // Non-standard[webkitTapHighlightColor]: This feature is non-standard and is not on a standards track.
54 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
55 | // @ts-ignore
56 | anchor.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'
57 | anchor.style.transition = touchActive.oldTransitionStyle ? touchActive.oldTransitionStyle + ', ' : '' + 'all .1s ease'
58 | touchActive.waitingToAddTimeId = setTimeout(() => {
59 | if (touchActive.element === anchor) {
60 | const elWidth = anchor.offsetWidth
61 | const elHeight = anchor.offsetHeight
62 | if (elWidth * elHeight > 90000) return
63 | anchor.style.filter = touchActive.oldFilterStyle + ' brightness(.8)'
64 | anchor.setAttribute('tap-highlight', 'true')
65 | }
66 | }, 30)
67 | }
68 | const cancelHighlight = (): void => {
69 | if (!touchActive.element) return
70 | clearTimeout(touchActive.waitingToAddTimeId)
71 | if (touchActive.element?.style.filter) {
72 | touchActive.element.style.filter = touchActive.oldFilterStyle
73 | }
74 | touchActive.element.removeAttribute('tap-highlight')
75 | setTimeout(() => {
76 | if (touchActive.element?.style.transition) {
77 | touchActive.element.style.transition = touchActive.oldTransitionStyle
78 | }
79 | touchActive.element = null
80 | touchActive.oldFilterStyle = ''
81 | touchActive.oldTransitionStyle = ''
82 | }, 100)
83 | }
84 | const delayCancelHighlight = (): void => {
85 | // Make entry control(appletWindow.setTimeout)
86 | appletWindow.setTimeout(() => {
87 | cancelHighlight()
88 | }, 300)
89 | }
90 | appletWindow.document.addEventListener('touchstart', addHighlight)
91 | appletWindow.document.addEventListener('touchmove', cancelHighlight)
92 | appletWindow.document.addEventListener('touchcancel', cancelHighlight)
93 | appletWindow.document.addEventListener('touchend', delayCancelHighlight)
94 | }
95 |
--------------------------------------------------------------------------------
/src/Applet/manifestProcess.ts:
--------------------------------------------------------------------------------
1 | import typeError from '../lib/typeError'
2 | import getIOSversion from '../lib/util/getIOSVersion'
3 | import { AppletManifest, FrameworksAppletConfig, AppletApplyOptions } from '../types'
4 |
5 | export default (manifest: AppletManifest, id: string): AppletManifest => {
6 | const config = manifest.config
7 | if (id === 'frameworks' || id === 'system') {
8 | if (config.free) {
9 | typeError(1101, 'warn')
10 | }
11 | if (config.source?.html || config.source?.src) {
12 | typeError(1102, 'warn')
13 | }
14 | }
15 | if (config.portal) {
16 | if (!config.free) {
17 | typeError(1103, 'warn')
18 | }
19 | }
20 | if ((config.level ?? 0) > 10000) {
21 | typeError(1104, 'warn')
22 | }
23 | if (id !== 'frameworks' && !(config.source?.html || config.source?.src)) {
24 | if (config.apply) {
25 | typeError(1202, 'warn')
26 | }
27 | } else {
28 | const defaultApply: AppletApplyOptions = ['smart-setTimeout', 'proxy-link', 'tap-highlight']
29 | const { apply = defaultApply, unApply } = config
30 | config.apply = apply
31 | if (unApply?.length) {
32 | config.apply = apply?.filter((item) => !unApply.includes(item))
33 | }
34 | }
35 | if (config.modality?.indexOf('sheet') === 0) {
36 | if (config.animation) {
37 | typeError(1105, 'warn')
38 | }
39 | config.animation = 'popup'
40 | } else if (config.animation === 'popup') {
41 | config.modality = 'sheet'
42 | typeError(1106, 'warn')
43 | }
44 | if (config.modality) {
45 | if (config.modality?.indexOf('paper') === 0) {
46 | if (config.animation) {
47 | typeError(1105, 'warn')
48 | }
49 | const { maskOpacity, swipeClosable } = config.paperOptions || { maskOpacity: 0.5 }
50 | config.animation = 'grow'
51 | config.sheetOptions = {
52 | stillBackdrop: true,
53 | noHandlebar: true,
54 | maskOpacity,
55 | swipeClosable,
56 | borderRadius: '0px',
57 | top: '0px'
58 | }
59 | } else if (config.modality?.indexOf('overlay') === 0) {
60 | if (config.animation) {
61 | typeError(1105, 'warn')
62 | }
63 | if (!config.color) {
64 | config.color = 'transparent'
65 | }
66 | const { maskOpacity, swipeClosable } = config.overlayOptions || { maskOpacity: 0.5 }
67 | config.animation = 'popup'
68 | config.sheetOptions = {
69 | stillBackdrop: true,
70 | noHandlebar: true,
71 | maskOpacity,
72 | swipeClosable,
73 | borderRadius: '0px',
74 | top: '0px',
75 | useFade: true
76 | }
77 | }
78 | if (!config.sheetOptions) {
79 | config.sheetOptions = {}
80 | }
81 | if (!getIOSversion()) {
82 | config.sheetOptions.stillBackdrop = true
83 | }
84 | }
85 | // In scenes such as Tab, you should also close the left slide to exit when no animation is set, otherwise overlay layers will appear.
86 | if (!config.animation) {
87 | config.disableSwipeModel = true
88 | }
89 | if (!(config as FrameworksAppletConfig).swipeTransitionType) {
90 | (config as FrameworksAppletConfig).swipeTransitionType = (config as FrameworksAppletConfig).swipeTransitionType ?? (getIOSversion() ? 'slide' : 'zoom')
91 | }
92 | return manifest
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/src/AppletControls/Base.ts:
--------------------------------------------------------------------------------
1 | import { Applet, Application, SmoothScroller } from '../types'
2 |
3 | export class AppletControlsBase {
4 | public applet: Applet
5 | public application: Application
6 | public scroll!: SmoothScroller
7 | public controlsView!: HTMLElement
8 | public controlsOverlay!: HTMLElement
9 | public contentContainer!: HTMLElement
10 | public backdropView!: HTMLElement
11 | public backdropReducedScale = 0.03
12 | public appletViewport: HTMLElement
13 | constructor(applet: Applet) {
14 | this.applet = applet
15 | this.application = applet.application
16 | this.appletViewport = this.applet.viewport as HTMLElement
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/AppletControls/EventTarget.ts:
--------------------------------------------------------------------------------
1 | import { AppletControlsState } from './State'
2 | export class AppletControlsEventTarget extends AppletControlsState {
3 | private available = true
4 | private toggleLock = false
5 | private requestGoBack(degree: number): void {
6 | const applet = this.applet
7 | if (applet.transforming) return
8 | if (this.toggleLock === false && degree <= this.advanceDegree && this.activity) {
9 | this.toggleLock = true
10 | setTimeout(() => {
11 | this.toggleLock = false
12 | }, 300)
13 | this.application.segue.back('controls').then(() => {
14 | this.fromViewports = undefined
15 | })
16 | }
17 | }
18 | private sliding(): void {
19 | const applet = this.applet
20 | const viewports = this.viewports
21 | const prevViewport = viewports[1]
22 | const swipeTransitionType = this.swipeTransitionType
23 | const degree = this.degree
24 | this.requestGoBack(degree)
25 | this.backdropView.animate([
26 | { opacity: degree }
27 | ], {
28 | duration: 0,
29 | fill: 'forwards'
30 | }).play()
31 | let controlsViewBackgroundColor = 'transparent'
32 | if (degree > 1) {
33 | controlsViewBackgroundColor = `rgba(0, 0, 0, ${0.5 + 3 * (degree - 1)})`
34 | }
35 | this.controlsView.animate([
36 | { backgroundColor: controlsViewBackgroundColor }
37 | ], {
38 | duration: 0,
39 | fill: 'forwards'
40 | }).play()
41 | if (prevViewport && applet.visibility && !applet.transforming && !this.toggleLock) {
42 | let prevViewportTransform = `scale(${1 - degree * 0.03})`
43 | if (swipeTransitionType === 'slide') {
44 | prevViewportTransform = `translate3d(${-degree * 30}%, 0, 0)`
45 | }
46 | prevViewport.animate([
47 | { transform: prevViewportTransform }
48 | ], {
49 | duration: 0,
50 | fill: 'forwards'
51 | }).play()
52 | }
53 | this.application.trigger('transition', degree, this.application.segue.appletGroup)
54 | }
55 | private slidingListener = this.sliding.bind(this)
56 | private bindSlidingEvent(): void {
57 | this.controlsView.addEventListener('scroll', this.slidingListener)
58 | }
59 | private removeSlidingEvent(): void {
60 | this.controlsView.removeEventListener('scroll', this.slidingListener)
61 | }
62 | protected bindBaseEvent(): void {
63 | this.backdropView.addEventListener('touchstart', async () => {
64 | await this.hide()
65 | this.requestGoBack(this.degree)
66 | })
67 | this.controlsOverlay.addEventListener('touchstart', (event) => {
68 | event.stopPropagation()
69 | event.preventDefault()
70 | }, false)
71 | // update viewports
72 | this.applet.on('willShow', () => {
73 | if (this.application.segue.stackUp) {
74 | this.clearFromViewports()
75 | }
76 | })
77 | // when history back
78 | this.applet.on('show', () => {
79 | if (!this.visibility) {
80 | this.appearImmediately()
81 | }
82 | if (this.application.segue.stackUp) {
83 | this.prepare()
84 | }
85 | })
86 | // isOverscrollHistoryNavigation
87 | this.applet.on('hide', () => {
88 | if (this.visibility && this.application.segue.fromOverscrollHistoryNavigation) {
89 | this.disappearImmediately()
90 | }
91 | })
92 | }
93 | public prepare(reset = false) {
94 | const viewports = this.viewports
95 | const swipeTransitionType = this.swipeTransitionType
96 | const prevViewport = viewports[1]
97 | if (prevViewport) {
98 | let transform = ''
99 | if (swipeTransitionType === 'slide') {
100 | transform = reset ? 'translate3d(0, 0, 0)' : 'translate3d(-30%, 0, 0)'
101 | } else {
102 | transform = reset ? 'scale(1)' : `scale(${1 - this.backdropReducedScale})`
103 | }
104 |
105 | prevViewport.animate([
106 | { transform }
107 | ], {
108 | duration: 0,
109 | fill: 'forwards'
110 | }).play()
111 | }
112 | }
113 | public async switch(show: boolean): Promise {
114 | this.controlsOverlay.style.display = 'block'
115 | this.toggleLock = true
116 | return this.scroll.snapTo(show ? this.appletViewport.offsetWidth : 0, 0).then(() => {
117 | this.toggleLock = false
118 | this.controlsOverlay.style.display = 'none'
119 | })
120 | }
121 | public async disappearImmediately() {
122 | this.controlsView.style.scrollBehavior = 'auto'
123 | this.controlsView.scrollLeft = 1 + this.advanceDegree
124 | }
125 | public async appearImmediately() {
126 | this.controlsView.style.scrollBehavior = 'auto'
127 | this.controlsView.scrollLeft = this.appletViewport.offsetWidth
128 | }
129 | public async show(): Promise {
130 | return this.switch(true).then(() => {
131 | this.bindSlidingEvent()
132 | })
133 | }
134 | public async hide(): Promise {
135 | return this.switch(false).then(() => {
136 | this.removeSlidingEvent()
137 | })
138 | }
139 | public disable() {
140 | this.available = false
141 | }
142 | public enable() {
143 | this.available = true
144 | }
145 | public activate(): void {
146 | if (this.available === false) return
147 | this.backdropView.style.display = 'block'
148 | this.controlsView.style.overflow = 'auto'
149 | this.controlsView.scrollLeft = this.appletViewport.offsetWidth
150 | this.bindSlidingEvent()
151 | }
152 | public freeze(): void {
153 | this.removeSlidingEvent()
154 | this.backdropView.style.display = 'none'
155 | this.controlsView.style.overflow = 'hidden'
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/AppletControls/State.ts:
--------------------------------------------------------------------------------
1 | import { AppletControlsBase } from './Base'
2 |
3 | export class AppletControlsState extends AppletControlsBase {
4 | public fromViewports?: Array
5 | public advanceDegree = 0
6 | public swipeTransitionType = this.application.config.swipeTransitionType
7 | get visibility(): boolean {
8 | return Math.round(this.controlsView.scrollLeft / this.appletViewport.offsetWidth) === 0 ? false : true
9 | }
10 | get viewports(): HTMLElement[] {
11 | if (this.fromViewports) {
12 | return this.fromViewports
13 | }
14 | return this.fromViewports = this.application.segue.viewports
15 | }
16 | get degree(): number {
17 | return this.controlsView.scrollLeft / this.appletViewport.offsetWidth
18 | }
19 | get activity(): boolean {
20 | return this.application.activityApplet === this.applet
21 | }
22 | public clearFromViewports(): void {
23 | this.fromViewports = undefined
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/AppletControls/View.ts:
--------------------------------------------------------------------------------
1 | import { AppletControlsEventTarget } from './EventTarget'
2 | import { SmoothScroller } from '../Scroll'
3 | import { testHasScrolling } from '../lib/util'
4 | import { snapItemCSSText, sheetViewCSSText, backdropViewCSSText, controlsViewCSSText, controlsOverlayCSSText } from './cssText'
5 |
6 | export class AppletControlsView extends AppletControlsEventTarget {
7 | public attach(): void {
8 | this.controlsView = document.createElement('applet-controls')
9 | this.contentContainer = document.createElement('applet-container')
10 | this.backdropView = document.createElement('applet-backdrop')
11 | this.controlsOverlay = document.createElement('applet-controls-overlay')
12 | this.scroll = new SmoothScroller(this.controlsView)
13 | }
14 | public buildControlsView(): void {
15 | const sheetViewStyle = document.createElement('style')
16 | sheetViewStyle.innerHTML = sheetViewCSSText
17 | this.contentContainer.style.cssText = snapItemCSSText
18 | this.backdropView.style.cssText = backdropViewCSSText
19 | this.controlsView.style.cssText = controlsViewCSSText
20 | /**
21 | * Obsolete
22 | * ------------- start -------------
23 | */
24 | // ios < 12.55 bug
25 | if (testHasScrolling() === false) {
26 | this.controlsView.style.cssText += '-webkit-overflow-scrolling: touch;'
27 | }
28 | /**
29 | * Obsolete
30 | * ------------- end -------------
31 | */
32 | this.controlsOverlay.style.cssText = controlsOverlayCSSText
33 | this.controlsView.appendChild(this.backdropView)
34 | this.controlsView.appendChild(this.contentContainer)
35 | this.appletViewport.appendChild(sheetViewStyle)
36 | this.appletViewport.appendChild(this.controlsView)
37 | this.appletViewport.appendChild(this.controlsOverlay)
38 | }
39 | public create(): HTMLElement {
40 | this.attach()
41 | this.buildControlsView()
42 | this.bindBaseEvent()
43 | const applet = this.applet
44 | if (applet.rel !== 'applet' || applet.swipeModel === false || applet.mountBehavior?.noSwipeModel) {
45 | this.freeze()
46 | }
47 | return this.contentContainer
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/AppletControls/cssText.ts:
--------------------------------------------------------------------------------
1 | import { coveredCSSText } from '../lib/cssText/coveredCSSText'
2 | import { fullscreenBaseCSSText } from '../lib/cssText/fullscreenBaseCSSText'
3 |
4 | // important: relative
5 | export const snapItemCSSText = `
6 | position: relative;
7 | display: flex;
8 | ${coveredCSSText}
9 | scroll-snap-align: start;
10 | scroll-snap-stop: always;
11 | `
12 |
13 | export const sheetViewCSSText = `
14 | applet-controls::-webkit-scrollbar {
15 | display: none;
16 | }
17 | applet-controls::scrollbar {
18 | display: none;
19 | }
20 | `
21 |
22 | export const backdropViewCSSText = `
23 | ${snapItemCSSText}
24 | background: rgba(0, 0, 0, .3);
25 | opacity: 0;
26 | `
27 | // important: relative
28 | export const controlsViewCSSText = `
29 | position: relative;
30 | display: flex;
31 | ${coveredCSSText}
32 | flex-direction: row;
33 | overflow: auto;
34 | scroll-behavior: auto;
35 | scroll-snap-type: both mandatory;
36 | `
37 |
38 | export const controlsOverlayCSSText = `
39 | position: fixed;
40 | ${fullscreenBaseCSSText}
41 | z-index: 9;
42 | display: none;
43 | `
--------------------------------------------------------------------------------
/src/AppletControls/index.ts:
--------------------------------------------------------------------------------
1 | import { AppletControlsView } from './View'
2 | export class AppletControls extends AppletControlsView {
3 | }
4 |
--------------------------------------------------------------------------------
/src/Application/Base.ts:
--------------------------------------------------------------------------------
1 | import { Applet } from '../Applet/index'
2 | import { EventProvider } from '../Event'
3 | import { AppletManifest, Segue, FrameworksAppletConfig, PresetConfig, PresetApplets } from '../types'
4 |
5 | type ApplicationOptions = Pick, 'applets'>
6 |
7 | class ApplicationBase extends EventProvider {
8 | public segue!: Segue
9 | public to!: Segue['to']
10 | public applets: { [key: string]: Applet } = {}
11 | public residentApplets: Array = []
12 | public presetAppletsView: PresetApplets = {}
13 | public config!: FrameworksAppletConfig
14 | public readonly historyNodeLocation: number = history.length
15 | protected routerRegExp = /([^#/]+)(.+)?/
16 | protected options!: ApplicationOptions
17 | protected resolveURL(url: string): URL {
18 | const link = new URL(
19 | url,
20 | window.location.toString()
21 | )
22 | const linkObject = link
23 | if (link.href === undefined) {
24 | linkObject.href = String(link)
25 | }
26 | return linkObject
27 | }
28 | protected verifyAppletSrcLegitimacy(url: string): boolean {
29 | const capture = this.config.capture
30 | if (capture) return this.captureAppletSrc(url, capture)
31 | const allowHosts = this.config.allowHosts || []
32 | if (allowHosts.length === 0) {
33 | return true
34 | }
35 | allowHosts.push(location.host)
36 | const link = new URL(
37 | decodeURIComponent(url),
38 | window.location.toString()
39 | )
40 | const linkHost = link.host
41 | for (const host of allowHosts) {
42 | if (linkHost === host) return true
43 | }
44 | return false
45 | }
46 | protected captureAppletSrc(url: string, capture = this.config.capture): boolean {
47 | const resolve = this.resolveURL(url)
48 | const path = resolve.origin + resolve.pathname
49 | if (typeof capture === 'string') {
50 | if (capture === path) return true
51 | } else if (typeof capture === 'function') {
52 | if (capture(resolve, url)) return true
53 | }
54 | return false
55 | }
56 | protected promiseApplet(promise: () => Promise): Promise {
57 | return Promise.resolve(promise())
58 | }
59 | public checkIsPresetAppletsView(id: string) {
60 | return this.residentApplets.includes(id)
61 | }
62 | public delPresetAppletsView(id: string, remove = false) {
63 | if (remove && !this.checkIsPresetAppletsView(id)) {
64 | const preset = this.presetAppletsView[id]
65 | if (preset && preset.parentElement) {
66 | preset.parentElement.removeChild(preset)
67 | }
68 | }
69 | delete this.presetAppletsView[id]
70 | }
71 | public setPrestAppletsView(presetApplets: PresetApplets) {
72 | this.residentApplets = Object.keys(presetApplets)
73 | this.presetAppletsView = presetApplets
74 | }
75 | public setting(options: ApplicationOptions): void {
76 | if (!options.applets.frameworks.config.prerender) {
77 | options.applets.frameworks.config.prerender = Object.keys(options.applets)
78 | }
79 | this.options = options
80 | }
81 | }
82 |
83 | export {
84 | ApplicationBase
85 | }
86 |
--------------------------------------------------------------------------------
/src/Application/State.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationBase } from './Base'
2 | import { Applet } from '../types'
3 |
4 | interface ApplicationRouteInfo {
5 | id: string
6 | param: string
7 | search: string
8 | }
9 |
10 | class ApplicationState extends ApplicationBase {
11 | get route(): ApplicationRouteInfo {
12 | const router = this.routerRegExp.exec(location.hash) || []
13 | const id = router[1]
14 | const param = router[2]
15 | const search = location.search
16 |
17 | return {
18 | id: id ? decodeURIComponent(id) : '',
19 | param: param ? decodeURIComponent(param) : '',
20 | search
21 | }
22 | }
23 | get exists(): boolean {
24 | try {
25 | return parseInt(sessionStorage.getItem(location.pathname + '__EXISTS') || '-1', 10) === history.length
26 | } catch (e) {
27 | return false
28 | }
29 | }
30 | get activityApplet(): Applet | undefined {
31 | const id = this.segue.id
32 | const applet = this.applets[id]
33 | return applet
34 | }
35 | get prevActivityApplet(): Applet | undefined {
36 | const id = this.segue.prevId
37 | const applet = this.applets[id]
38 | return applet
39 | }
40 | get activityLevel() {
41 | return (this.activityApplet?.level ?? 0) + 1
42 | }
43 | get isFullscreen(): boolean {
44 | return document.body.offsetHeight === screen.height
45 | }
46 | public overscrollHistoryNavigation = {
47 | moment: 0,
48 | type: ''
49 | }
50 | public properties = {
51 | darkTheme: window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
52 | }
53 | public setExists(): Promise {
54 | const key = location.pathname + '__EXISTS'
55 | const len = String(history.length)
56 | return new Promise((resolve, reject) => {
57 | try {
58 | sessionStorage.setItem(key, len)
59 | resolve()
60 | } catch (e) {
61 | try {
62 | sessionStorage.clear()
63 | sessionStorage.setItem(key, len)
64 | } catch (error) {
65 | reject()
66 | }
67 | }
68 | })
69 | }
70 | public activeState: 'active' | 'frozen' = 'active'
71 | public activate(active: boolean) {
72 | const state = active ? 'active' : 'frozen'
73 | const messageType = 'application-' + state
74 | if (window.applicationActiveState === state) return
75 | this.activeState = state
76 | this.trigger(state)
77 | window.applicationActiveState = state
78 | window.postMessage({
79 | type: messageType
80 | }, '*')
81 |
82 | // Tunneling child app
83 | for (const id in this.applets) {
84 | const applet = this.applets[id]
85 | if (applet.sandbox && !applet.sameOrigin) {
86 | applet.triggerWindow(messageType)
87 | }
88 | }
89 | }
90 | }
91 |
92 | export {
93 | ApplicationState
94 | }
95 |
--------------------------------------------------------------------------------
/src/Application/provider.ts:
--------------------------------------------------------------------------------
1 | import { Application } from './index'
2 | import { PushWindowOptions } from '../types'
3 |
4 | export default (app: Application): void => {
5 | window.addEventListener('message', (event: MessageEvent) => {
6 | if (event.source === window) return
7 | const { action, data } = event.data
8 | switch (action) {
9 | case 'to':
10 | app.segue.to(data.applet, data.query, data.history)
11 | break
12 | case 'back':
13 | app.segue.back()
14 | break
15 | case 'forward':
16 | app.segue.forward()
17 | break
18 | case 'pushWindow':
19 | app.pushWindow(...(data as PushWindowOptions))
20 | break
21 | }
22 | })
23 | return
24 | }
25 |
--------------------------------------------------------------------------------
/src/Define/DefineApplet.ts:
--------------------------------------------------------------------------------
1 | import Preset from './preset'
2 | import { getEnv } from './env'
3 | import { buildAppletSlot } from './slot'
4 | import testHasScrolling from '../lib/util/testHasScrolling'
5 | import typeError from '../lib/typeError'
6 |
7 | export class DefineApplet extends HTMLElement {
8 | constructor() {
9 | super()
10 | }
11 | private installed = false
12 | public appletSlot!: HTMLSlotElement
13 | public getViewSlot() {
14 | return this.appletSlot
15 | }
16 | static get observedAttributes() {
17 | return ['applet-id']
18 | }
19 | private get appletId() {
20 | return this.getAttribute('applet-id')
21 | }
22 | private defineApplet() {
23 | const id = this.appletId
24 | const { USE_SHADOW_DOM } = getEnv()
25 | if (!id) return
26 | /**
27 | * Obsolete
28 | * ------------- start -------------
29 | */
30 | // old; ios < 9
31 | if (!USE_SHADOW_DOM) {
32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
33 | Preset.appletsSlot[id] = this.appletSlot = this as any
34 | Preset.appletsDefinition[id] = this
35 | return
36 | }
37 | /**
38 | * Obsolete
39 | * ------------- end -------------
40 | */
41 |
42 | this.slot = 'applet-' + id
43 | this.appletSlot = buildAppletSlot(id)
44 | Preset.appletsDefinition[id] = this
45 | }
46 | attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
47 | if (name === 'applet-id' && newValue) {
48 | // ${id}-obsolete
49 | if (oldValue && newValue.split('-')[0] !== oldValue.split('-')[0]) {
50 | typeError(1002, 'warn', `oldValue: ${oldValue}, newValue: ${newValue}`)
51 | typeError(1003, 'warn')
52 | }
53 | // some exception callbacks
54 | this.defineApplet?.()
55 | }
56 | }
57 | connectedCallback(): void {
58 | if (this.installed) return
59 | this.installed = true
60 | this.defineApplet()
61 | if (testHasScrolling() === false) {
62 | /**
63 | * Obsolete
64 | * ------------- start -------------
65 | */
66 | // ios < 12.55 bug
67 | setTimeout(() => {
68 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
69 | // @ts-ignore
70 | this.style.webkitOverflowScrolling = 'touch'
71 | }, 0)
72 | /**
73 | * Obsolete
74 | * ------------- end -------------
75 | */
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Define/DefineApplication.ts:
--------------------------------------------------------------------------------
1 | import Preset from './preset'
2 | import { getEnv } from './env'
3 | import { buildAppletSlot } from './slot'
4 | import typeError from '../lib/typeError'
5 | export class DefineApplication extends HTMLElement {
6 | private contentShadowRoot!: ShadowRoot
7 | private installed = false
8 | constructor() {
9 | super()
10 | }
11 | static get observedAttributes() {
12 | return ['default-applet']
13 | }
14 | private init() {
15 | const { USE_SHADOW_DOM } = getEnv()
16 | if (this.contentShadowRoot) return
17 | /**
18 | * Obsolete
19 | * ------------- start -------------
20 | */
21 | if (USE_SHADOW_DOM) {
22 | this.contentShadowRoot = this.attachShadow?.({ mode: 'closed' })
23 | } else {
24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
25 | this.contentShadowRoot = this as any
26 | }
27 | /**
28 | * Obsolete
29 | * ------------- end -------------
30 | */
31 | this.contentShadowRoot.appendChild(buildAppletSlot('system'))
32 | this.contentShadowRoot.appendChild(buildAppletSlot('frameworks'))
33 | Preset.root = this.contentShadowRoot
34 | Preset.appletsSpace = this
35 | }
36 | private get defaultApplet() {
37 | return this.getAttribute('default-applet')
38 | }
39 | private defineApplet() {
40 | if (!this.defaultApplet) return
41 | this.init()
42 | this.setupDefaultApplet(this.defaultApplet ?? 'home')
43 | }
44 | setupDefaultApplet(name: string) {
45 | this.contentShadowRoot.appendChild(buildAppletSlot(name))
46 | Preset.defaultApplet = name
47 | Preset.awaitCallback?.()
48 | }
49 | attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
50 | if (name === 'default-applet' && newValue) {
51 | if (oldValue && oldValue !== newValue) {
52 | typeError(1004, 'warn')
53 | }
54 | // some exception callbacks
55 | this.defineApplet?.()
56 | }
57 | }
58 | connectedCallback(): void {
59 | if (this.installed) return
60 | this.installed = true
61 | this.defineApplet()
62 | }
63 | }
--------------------------------------------------------------------------------
/src/Define/env.ts:
--------------------------------------------------------------------------------
1 | import needsPolyfill from '../lib/wc/needsPolyfill'
2 |
3 | /**
4 | * Obsolete
5 | * ------------- start -------------
6 | */
7 | let _USE_SHADOW_DOM = true
8 | if (needsPolyfill && /iPhone OS [0-9]_\d(_\d)? like mac os x/ig.exec(navigator.userAgent)) {
9 | _USE_SHADOW_DOM = false
10 | }
11 |
12 | if (window.__LATH_NO_SHADOW_DOM__) {
13 | _USE_SHADOW_DOM = false
14 | }
15 |
16 | /**
17 | * Obsolete
18 | * ------------- end -------------
19 | */
20 |
21 | let _USE_PERCENTAGE = false
22 | interface EnvOptions {
23 | USE_SHADOW_DOM?: boolean,
24 | USE_PERCENTAGE?: boolean
25 | }
26 |
27 | export function setEnv(options: EnvOptions) {
28 | _USE_SHADOW_DOM = options.USE_SHADOW_DOM ?? _USE_SHADOW_DOM
29 | _USE_PERCENTAGE = options.USE_PERCENTAGE ?? _USE_PERCENTAGE
30 | }
31 | export function getEnv() {
32 | return {
33 | USE_SHADOW_DOM: _USE_SHADOW_DOM,
34 | USE_PERCENTAGE: _USE_PERCENTAGE
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Define/index.ts:
--------------------------------------------------------------------------------
1 | import Preset from './preset'
2 | import { AppletAllTypeSettings, PresetConfig, Application } from '../types'
3 | import typeError from '../lib/typeError'
4 | import autoScrollPolyfill from '../Scroll/polyfill'
5 | import loadWebAnimations from '../lib/webAnimations/load'
6 | import { initApplication } from './init'
7 |
8 | import('..').catch((e) => {
9 | console.warn(e)
10 | })
11 | autoScrollPolyfill()
12 | loadWebAnimations()
13 |
14 | export * from './env'
15 | export const destroyApplication = () => {
16 | if (!Preset.appletsSpace) return
17 | const appletsSpace = Preset.appletsSpace
18 | const parentElement = appletsSpace.parentElement
19 | const wrapper = document.createElement('div')
20 | parentElement?.insertBefore(wrapper, appletsSpace)
21 | const childLength = appletsSpace.children.length
22 | for (let i = 0; i <= childLength; i++) {
23 | wrapper.appendChild(appletsSpace.children[i])
24 | }
25 | }
26 |
27 | export const createApplication = async (options: Partial = { tunneling: false }): Promise => {
28 | initApplication()
29 | /**
30 | * Obsolete
31 | */
32 | await autoScrollPolyfill()
33 | await loadWebAnimations()
34 | if (Preset.__EXISTING__) return Promise.reject('repeat')
35 | if (options.tunneling && !!window.__LATH_APPLICATION_AVAILABILITY__) return Promise.reject('tunneling')
36 | Preset.__EXISTING__ = true
37 | let Application
38 | try {
39 | ({ Application } = await import('..'))
40 | } catch (error) {
41 | console.warn(error)
42 | return Promise.reject(error)
43 | }
44 | const { tunneling = false, zIndex = undefined, applets = {} as Required['applets'] } = options
45 | if (!Preset.root) {
46 | setTimeout(() => {
47 | if (!Preset.root) {
48 | typeError(1005, 'return')
49 | }
50 | }, 5000)
51 | await Preset.awaitDefine()
52 | }
53 | const { root, defaultApplet, appletsSpace } = Preset
54 | if (!root) {
55 | return Promise.reject(typeError(1005, 'return'))
56 | }
57 | const application = new Application({ root, tunneling, zIndex, appletsSpace })
58 | const index = applets?.frameworks?.config?.index
59 | if (!Preset.appletsDefinition[defaultApplet]) {
60 | return Promise.reject(typeError(1006, 'return'))
61 | }
62 | if (!applets.frameworks) {
63 | applets.frameworks = {
64 | config: {
65 | level: 0,
66 | index: defaultApplet,
67 | singleFlow: false,
68 | singleLock: false,
69 | animation: 'slide',
70 | transientTimeout: 1800000,
71 | disableTransient: true
72 | }
73 | }
74 | if (!applets[defaultApplet]) {
75 | applets[defaultApplet] = {
76 | config: {
77 | level: 1,
78 | title: document.title,
79 | free: false,
80 | animation: 'slide',
81 | background: true
82 | }
83 | } as AppletAllTypeSettings
84 | }
85 | }
86 | if (index && !applets[index]) {
87 | applets[index] = {
88 | config: {
89 | level: 1,
90 | title: document.title,
91 | free: false,
92 | animation: 'slide',
93 | background: true
94 | }
95 | } as AppletAllTypeSettings
96 | }
97 | for (const name in Preset.appletsDefinition) {
98 | if (!applets[name]) {
99 | applets[name] = {
100 | config: {
101 | level: 1,
102 | title: document.title,
103 | free: false,
104 | animation: 'slide',
105 | background: true
106 | }
107 | } as AppletAllTypeSettings
108 | }
109 | }
110 | application.setting({
111 | applets
112 | })
113 |
114 | await application.start()
115 | application.setPrestAppletsView(Preset.appletsDefinition)
116 | return Promise.resolve(application)
117 | }
118 |
--------------------------------------------------------------------------------
/src/Define/init.ts:
--------------------------------------------------------------------------------
1 | import './prepare'
2 | // 1. import custom-elements iOS < 10.2
3 | import '@webcomponents/custom-elements'
4 |
5 | // 2. define
6 | import { DefineApplet } from './DefineApplet'
7 | import { DefineApplication } from './DefineApplication'
8 | import typeError from '../lib/typeError'
9 | import needsPolyfill from '../lib/wc/needsPolyfill'
10 |
11 | if (needsPolyfill) {
12 | import('@webcomponents/webcomponentsjs').catch((e) => {
13 | console.warn(e)
14 | })
15 | }
16 |
17 | export const initApplication = () => {
18 | const defineElements = () => {
19 | if (customElements.get('define-applet') || customElements.get('define-application')) {
20 | typeError(1003)
21 | return
22 | }
23 | customElements.define('define-applet', DefineApplet)
24 | customElements.define('define-application', DefineApplication)
25 | }
26 | if (needsPolyfill) {
27 | import('@webcomponents/webcomponentsjs').then(defineElements).catch((e) => {
28 | console.warn(e)
29 | })
30 | } else {
31 | defineElements()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Define/prepare.ts:
--------------------------------------------------------------------------------
1 | import needsPolyfill from '../lib/wc/needsPolyfill'
2 |
3 | // The core logic is separated from the above-the-fold dependency.
4 | import('..').catch((e) => {
5 | console.warn(e)
6 | })
7 |
8 | /**
9 | * Android 4.0 & iOS 10.2 and below require Polyfill.
10 | * Currently, less than 0.1% of device types are used, so this section is loaded on demand.
11 | */
12 | if (needsPolyfill) {
13 | import('@webcomponents/webcomponentsjs').catch((e) => {
14 | console.warn(e)
15 | })
16 | }
17 |
18 | if (typeof Element.prototype.scrollTo !== 'function') {
19 | import('scroll-polyfill/auto').catch((e) => {
20 | console.warn(e)
21 | })
22 | }
--------------------------------------------------------------------------------
/src/Define/preset.ts:
--------------------------------------------------------------------------------
1 | import { DefineApplet } from '../types'
2 |
3 | interface PresetConst {
4 | root?: ShadowRoot
5 | appletsDefinition: {
6 | [key: string]: DefineApplet
7 | }
8 | appletsSlot: {
9 | [key: string]: HTMLSlotElement
10 | }
11 | appletsSpace?: HTMLElement
12 | defaultApplet: string
13 | awaitCallback?: () => void
14 | awaitDefine: () => Promise
15 | __EXISTING__: boolean
16 | }
17 |
18 | const Preset: PresetConst = {
19 | appletsDefinition: {},
20 | appletsSlot: {},
21 | defaultApplet: 'home',
22 | awaitDefine: () => new Promise((resolve) => Preset.awaitCallback = resolve),
23 | __EXISTING__: false
24 | }
25 |
26 | export default Preset
--------------------------------------------------------------------------------
/src/Define/slot.ts:
--------------------------------------------------------------------------------
1 | import Preset from './preset'
2 |
3 | export const buildAppletSlot = (name: string) => {
4 | if (Preset.appletsSlot[name]) {
5 | return Preset.appletsSlot[name]
6 | }
7 | const appletSlot = document.createElement('slot')
8 | appletSlot.name = 'applet-' + name
9 | Preset.appletsSlot[name] = appletSlot
10 | return appletSlot
11 | }
12 |
--------------------------------------------------------------------------------
/src/Event/index.ts:
--------------------------------------------------------------------------------
1 | import { TriggerEventTypes, TriggerEventCallbackArgs, TriggerEventCallback } from '../types'
2 |
3 | type EventMap = {
4 | [key in TriggerEventTypes]?: TriggerEventCallback[]
5 | }
6 |
7 | const EventGroupSymbol = Symbol('__EventGroupName__')
8 | class EventProvider {
9 | private _events: EventMap = {}
10 | public on>(type: T, fn: F, groupName?: string): this {
11 | Object.defineProperty(fn, EventGroupSymbol, { value: groupName, writable: true })
12 | if (!this._events[type]) this._events[type] = []
13 | this._events[type]?.push(fn)
14 | return this
15 | }
16 |
17 | public one>(type: T, fn: F, groupName?: string): this {
18 | const once: TriggerEventCallback = ((...args: TriggerEventCallbackArgs) => {
19 | fn(...args)
20 | this.off(type, once)
21 | })
22 | Object.defineProperty(once, EventGroupSymbol, { value: groupName, writable: true })
23 |
24 | if (!this._events[type]) this._events[type] = []
25 | this._events[type]?.push(once)
26 |
27 | return this
28 | }
29 |
30 | public off>(type: T, fn: F): this {
31 | if (!this._events[type]) return this
32 | const index = this._events[type]?.indexOf(fn) ?? -1
33 | if (index > -1) this._events[type]?.splice(index, 1)
34 | return this
35 | }
36 |
37 | public removeEventGroup(groupName: string): this {
38 | const filter = (t: T) => {
39 | return (this._events[t] as (TriggerEventCallback & { [EventGroupSymbol]: string })[] | undefined)?.filter((fn) => {
40 | return fn[EventGroupSymbol] !== groupName
41 | })
42 | }
43 |
44 | for (const type in this._events) {
45 | this._events[type as TriggerEventTypes] = filter(type as TriggerEventTypes)
46 | }
47 |
48 | return this
49 | }
50 |
51 | public trigger(type: T, ...args: TriggerEventCallbackArgs): this {
52 | if (!this._events[type]) return this
53 |
54 | this._events[type]?.forEach((fn) => {
55 | try {
56 | fn(...args)
57 | } catch (e) {
58 | this.off(type, fn)
59 | this.trigger('error', e)
60 | }
61 | })
62 | return this
63 | }
64 | }
65 |
66 | export {
67 | EventProvider
68 | }
69 |
--------------------------------------------------------------------------------
/src/Modality/Base.ts:
--------------------------------------------------------------------------------
1 | import { Applet, Application, SheetOptions, SmoothScroller } from '../types'
2 | class ModalityBase {
3 | public applet: Applet
4 | public application: Application
5 | protected scroller!: SmoothScroller
6 | protected switching = false
7 | protected scrolling = false
8 | protected maxDegreeCache?: number
9 | public backdropReducedScale = 0.1
10 | public backdropRotateX = -10
11 | public backdropPerspective = 3000
12 | public backdropBorderRadius = 20
13 | public advanceDegree = 800 / window.innerHeight * 0.03
14 | public options?: SheetOptions
15 | public modalityContainer: HTMLElement
16 | public contentContainer!: HTMLElement
17 | public miniCard?: HTMLElement
18 | public defaultToLarge?: boolean
19 | public appletViewport: HTMLElement
20 | public fromViewports?: Array
21 | public modalityOverlay!: HTMLElement
22 | public modalityPlaceholder!: HTMLElement
23 | constructor(applet: Applet) {
24 | this.applet = applet
25 | this.application = this.applet.application
26 | this.modalityContainer = document.createElement('modality-container')
27 | this.appletViewport = this.applet.viewport as HTMLElement
28 | this.options = this.applet.config.sheetOptions
29 | this.defaultToLarge = this.options?.defaultCardSize === 'large'
30 | }
31 | }
32 |
33 | export {
34 | ModalityBase
35 | }
36 |
--------------------------------------------------------------------------------
/src/Modality/EventTarget.ts:
--------------------------------------------------------------------------------
1 | import { ModalityState } from './State'
2 | import { setTimeout, testHasSmoothSnapScrolling, testHasSnapReset, sleep } from '../lib/util'
3 |
4 | /**
5 | * Obsolete
6 | * ------------- start -------------
7 | */
8 | // old; ios < 9
9 | const NoSmoothSnapScrolling = testHasSmoothSnapScrolling() === false
10 | // old; ios < 15
11 | const HasSnapReset = testHasSnapReset()
12 | /**
13 | * Obsolete
14 | * ------------- end -------------
15 | */
16 | class ModalityEventTarget extends ModalityState {
17 | private scrollingTimeoutId = -1
18 | private freezePosition = -1
19 | // old; ios < 15
20 | private fixSnapReset = HasSnapReset ? false : true
21 | private fallHeight = this.application.isFullscreen ? 40 : 0
22 | private processPromise!: Promise
23 | private async switchSheet(hideThresholdScale = 1.5): Promise {
24 | if (!this.activity) return Promise.resolve()
25 | if (this.degree <= this.maxDegree / hideThresholdScale) {
26 | return this.hide()
27 | } else {
28 | return this.rise()
29 | }
30 | }
31 | private switchOverlay(open = false): void {
32 | this.modalityOverlay.style.display = open ? 'block' : 'none'
33 | }
34 | private switchSmooth(open = true): void {
35 | this.modalityContainer.style.scrollBehavior = open ? 'smooth' : 'auto'
36 | }
37 | private switchSnap(open = true): void {
38 | this.modalityContainer.style.scrollSnapType = open ? 'y mandatory' : 'none'
39 | }
40 | private switchBackdropColor(open = true): void {
41 | this.application.segue.applicationViewport.style.backgroundColor = open ? this.options?.backdropColor ?? '#000' : ''
42 | }
43 | public freezeSnap() {
44 | this.modalityPlaceholder.style.display = 'none'
45 | this.modalityContainer.scrollTop = 0
46 | }
47 | public async activateSnap(): Promise {
48 | this.switchSmooth(false)
49 | /**
50 | * Obsolete
51 | * ------------- start -------------
52 | */
53 | if (!HasSnapReset) this.switchSnap(false)
54 | /**
55 | * Obsolete
56 | * ------------- end -------------
57 | */
58 | this.modalityPlaceholder.style.display = 'flex'
59 | this.modalityContainer.scrollTop = this.modalityContainer.offsetHeight + (this.miniCard?.offsetHeight ?? 0)
60 | await sleep(0)
61 | this.switchSmooth(true)
62 | /**
63 | * Obsolete
64 | * ------------- start -------------
65 | */
66 | // old; ios < 15
67 | if (!HasSnapReset) this.switchSnap(true)
68 | /**
69 | * Obsolete
70 | * ------------- end -------------
71 | */
72 | }
73 | public async rise(): Promise {
74 | if (this.switching) return this.processPromise
75 | this.switching = true
76 | this.switchOverlay(true)
77 | const offsetTop = this.modalityContainer.offsetHeight + (this.defaultToLarge ? this.contentContainer.offsetHeight : 0) + (this.miniCard ? (this.degree >= 1 + (this.maxDegree - 1) / 2 ? (this.miniCard?.offsetHeight ?? 0) : 0) : 0)
78 | return this.processPromise = this.scroller.snapTo(0, offsetTop).then(() => {
79 | this.switching = false
80 | this.switchOverlay(false)
81 | })
82 | }
83 | public async fall(): Promise {
84 | if (this.switching) return this.processPromise
85 | this.switching = true
86 | this.switchOverlay(true)
87 | return this.processPromise = this.scroller.snapTo(0, 0).then(async () => {
88 | this.switching = false
89 | this.switchOverlay(false)
90 | })
91 | }
92 | public async hide(): Promise {
93 | if (this.switching) return this.processPromise
94 | this.switching = true
95 | if (!this.activity) return Promise.resolve()
96 | this.removeSlidingEvent()
97 | this.segueTransition(false)
98 | return this.fall().then(async () => {
99 | this.switching = false
100 | if (this.activity) {
101 | await this.application.segue.back()
102 | }
103 | })
104 | }
105 | private segueTransition(show = true) {
106 | const duration = 500
107 | const delay = 100 // wait switchBackdropColor
108 | const prevViewport = this.prevViewport
109 | const relativeDegree = show ? (this.miniCard && !this.defaultToLarge ? 0 : 1) : 0
110 | const options = this.options
111 | const darkness = options?.maskOpacity ?? 0.3
112 | const useFade = options?.useFade
113 | // stillBackdrop
114 | const stillBackdrop = this.options?.stillBackdrop
115 | if (!stillBackdrop) {
116 | prevViewport.animate([
117 | {
118 | borderRadius: `${relativeDegree * this.backdropBorderRadius}px`,
119 | transform: `
120 | translate3d(0, ${relativeDegree * this.fallHeight}px, -100px)
121 | perspective(${this.backdropPerspective}px)
122 | rotateX(${relativeDegree * this.backdropRotateX}deg)
123 | scale(${1 - Math.max(relativeDegree * this.backdropReducedScale, 0)})
124 | `
125 | }
126 | ], {
127 | duration,
128 | delay,
129 | fill: 'forwards'
130 | }).play()
131 | }
132 | this.modalityContainer.animate([
133 | {
134 | backgroundColor: `rgba(0, 0, 0, ${show ? darkness : 0})`
135 | }
136 | ], {
137 | duration,
138 | delay,
139 | fill: 'forwards'
140 | }).play()
141 | // reset opacity while "hide"
142 | if (useFade && !show) {
143 | // this.contentContainer.style.opacity = '1'
144 | this.contentContainer.animate([
145 | {
146 | opacity: '1'
147 | }
148 | ], {
149 | duration: 0,
150 | fill: 'forwards'
151 | }).play()
152 | }
153 | }
154 | private sliding() {
155 | // this.activity: Prevents asynchronous operations from resetting closed views
156 | if (!this.activity) return
157 | const degree = this.degree
158 | const maxDegree = this.maxDegree
159 | const prevViewport = this.prevViewport
160 | const options = this.options
161 | const darkness = options?.maskOpacity ?? 0.3
162 | const useFade = options?.useFade
163 | if (degree > maxDegree) {
164 | this.contentContainer.animate([
165 | {
166 | backgroundColor: `rgba(0, 0, 0, ${darkness + (1 - darkness) * (degree - maxDegree) / 0.2})`
167 | }
168 | ], {
169 | duration: 0,
170 | fill: 'forwards'
171 | }).play()
172 | return
173 | }
174 | // if miniCard.
175 | const relativeDegree = this.miniCard ? (degree - 1) / (maxDegree - 1) : degree
176 | // stillBackdrop
177 | const stillBackdrop = this.options?.stillBackdrop ?? (this.miniCard && degree <= 1)
178 | // this.activity: Prevents asynchronous operations from resetting closed views
179 | if (this.activity && prevViewport && !stillBackdrop) {
180 | prevViewport.animate([
181 | {
182 | borderRadius: `${Math.min(relativeDegree, 1) * this.backdropBorderRadius}px`,
183 | transform: `
184 | translate3d(0, ${relativeDegree * this.fallHeight}px, -100px)
185 | perspective(${this.backdropPerspective}px)
186 | rotateX(${relativeDegree * this.backdropRotateX}deg)
187 | scale(${1 - Math.max(relativeDegree * this.backdropReducedScale, 0)})
188 | `
189 | }
190 | ], {
191 | duration: 0,
192 | fill: 'forwards'
193 | }).play()
194 | }
195 | this.modalityContainer.animate([
196 | {
197 | backgroundColor: `rgba(0, 0, 0, ${darkness * Math.min(degree, 1)})`
198 | }
199 | ], {
200 | duration: 0,
201 | fill: 'forwards'
202 | }).play()
203 | if (useFade) {
204 | this.contentContainer.animate([
205 | {
206 | opacity: `${relativeDegree - ((1 - relativeDegree) * 2)}`
207 | }
208 | ], {
209 | duration: 0,
210 | fill: 'forwards'
211 | }).play()
212 | }
213 |
214 | this.scrolling = true
215 | clearTimeout(this.scrollingTimeoutId)
216 | this.scrollingTimeoutId = setTimeout(() => {
217 | this.scrolling = false
218 | /**
219 | * Obsolete
220 | * ------------- start -------------
221 | */
222 | // old; ios < 9
223 | if (NoSmoothSnapScrolling) {
224 | if (this.degree === degree) {
225 | if (degree <= 0.7) {
226 | this.hide()
227 | } else {
228 | this.rise()
229 | }
230 | }
231 | }
232 | /**
233 | * Obsolete
234 | * ------------- end -------------
235 | */
236 | }, 100)
237 | // when triggered by a blocked holder
238 | if (this.switching) return
239 | if (degree <= this.advanceDegree) {
240 | this.hide()
241 | }
242 | }
243 | private slidingListener = this.sliding.bind(this)
244 | private bindSlidingEvent(): void {
245 | this.modalityContainer.addEventListener('scroll', this.slidingListener)
246 | }
247 | private removeSlidingEvent(): void {
248 | this.modalityContainer.removeEventListener('scroll', this.slidingListener)
249 | }
250 | protected bindDragContentEvent(): void {
251 | const dragContent = this.applet.contentDocument
252 | if (!dragContent) return
253 | const startPoint: {
254 | x: number
255 | y: number
256 | swipe: boolean | undefined
257 | } = {
258 | x: 0,
259 | y: 0,
260 | swipe: undefined
261 | }
262 | const modalityContainer = this.modalityContainer
263 | const speedRate = modalityContainer.offsetHeight / modalityContainer.offsetWidth
264 | dragContent.addEventListener('touchstart', (event: Event) => {
265 | const { changedTouches } = event as TouchEvent
266 | const touch = changedTouches[0]
267 | startPoint.x = touch.pageX
268 | startPoint.y = touch.pageY
269 | startPoint.swipe = undefined
270 | }, true)
271 | dragContent.addEventListener('touchend', async () => {
272 | if (!startPoint.swipe) return
273 | startPoint.swipe = false
274 | if (!await this.checkScrollStop()) return
275 | this.switchSmooth(true)
276 | this.switchSnap(true)
277 | this.switchSheet(this.miniCard ? 2 : 1.25).then(() => {
278 | this.switchOverlay(false)
279 | this.switching = false
280 | })
281 | }, true)
282 | dragContent.addEventListener('touchmove', (event: Event) => {
283 | const { changedTouches } = event as TouchEvent
284 | const touch = changedTouches[0]
285 | const deltaX = touch.pageX - startPoint.x
286 | const deltaY = touch.pageY - startPoint.y
287 | if (this.switching) return
288 | if (startPoint.swipe === false) return
289 | if (Math.abs(deltaX) - Math.abs(deltaY) > 20 && Math.abs(deltaY) <= 10) {
290 | startPoint.swipe = true
291 | this.switchOverlay(true)
292 | this.switchSmooth(false)
293 | this.switchSnap(false)
294 | }
295 | if (startPoint.swipe) {
296 | modalityContainer.scrollTop -= Math.ceil(deltaX) * speedRate
297 | startPoint.x = touch.pageX
298 | startPoint.y = touch.pageY
299 | }
300 | }, true)
301 | }
302 | protected bindBaseEvent() {
303 | this.applet.on('willShow', () => {
304 | this.switchBackdropColor(true)
305 | if (this.application.segue.stackUp) {
306 | this.fromViewports = undefined
307 | }
308 | })
309 | this.applet.on('willSegueShow', () => {
310 | this.segueTransition(true)
311 | })
312 | this.applet.on('show', () => {
313 | this.bindSlidingEvent()
314 | })
315 | this.applet.on('willHide', () => {
316 | this.removeSlidingEvent()
317 | })
318 | this.applet.on('willSegueHide', () => {
319 | this.segueTransition(false)
320 | })
321 | this.applet.on('hide', () => {
322 | this.switchBackdropColor(false)
323 | })
324 | }
325 | public freeze(): void {
326 | this.removeSlidingEvent()
327 | if (this.fixSnapReset === false) {
328 | // (offsetTop) ios < 15, Get the scroll position of the snap as 0.
329 | this.freezePosition = this.modalityContainer.scrollTop || this.contentContainer.offsetTop
330 | this.modalityPlaceholder.style.display = 'none'
331 | }
332 | }
333 | public activate(): void {
334 | if (this.fixSnapReset === false && this.freezePosition !== -1) {
335 | this.modalityPlaceholder.style.display = 'block'
336 | const freezePosition = this.freezePosition
337 | this.modalityContainer.scrollTop = freezePosition
338 | this.scroller.snapTo(0, freezePosition, 0)
339 | this.freezePosition = -1
340 | // The snap will be automatically repaired on the second execution.
341 | this.fixSnapReset = true
342 | }
343 | this.bindSlidingEvent()
344 | }
345 | }
346 |
347 | export {
348 | ModalityEventTarget
349 | }
350 |
--------------------------------------------------------------------------------
/src/Modality/State.ts:
--------------------------------------------------------------------------------
1 | import { ModalityBase } from './Base'
2 |
3 | class ModalityState extends ModalityBase {
4 | /**
5 | * Gets the view of the two switched windows during a transition.
6 | * The value should be cached because it needs to be returned from the state.
7 | * If the previous view was a modal box, you need to replace it with the previous non-modal box view.
8 | */
9 | get viewports() {
10 | if (this.fromViewports && this.activity) {
11 | return this.fromViewports
12 | }
13 | this.fromViewports = this.application.segue.viewports
14 | this.updateOverlapViewport()
15 | return this.fromViewports
16 | }
17 | get maxDegree() {
18 | if (this.maxDegreeCache) return this.maxDegreeCache
19 | const miniCardHeight = this.miniCard?.offsetHeight ?? 0
20 | return this.maxDegreeCache = 1 + miniCardHeight / this.contentContainer.offsetHeight
21 | }
22 | get degree() {
23 | return this.modalityContainer.scrollTop / Math.min(this.contentContainer.offsetHeight + (this.miniCard?.offsetHeight ?? 0), this.modalityContainer.offsetHeight)
24 | }
25 | get visibility(): boolean {
26 | return this.degree <= 0.1 ? false : true
27 | }
28 | get hasMiniCard(): boolean {
29 | return !!this.miniCard
30 | }
31 | get prevViewport(): HTMLElement {
32 | return this.viewports[1]
33 | }
34 | get activity(): boolean {
35 | return this.applet.transforming || this.application.activityApplet === this.applet
36 | }
37 | private setBackdropViewport(viewport: HTMLElement): void {
38 | if (!this.fromViewports) {
39 | this.fromViewports = this.viewports
40 | }
41 | this.fromViewports[1] = viewport
42 | }
43 | private updateOverlapViewport(): void {
44 | const prevActivityApplet = this.application.prevActivityApplet
45 | if (prevActivityApplet?.modality) {
46 | this.setBackdropViewport(prevActivityApplet.modality.viewports[1])
47 | }
48 | }
49 | protected checkScrollStop(): Promise {
50 | return new Promise((resolve) => {
51 | const waitScrollEnd = (callback: () => void) => {
52 | setTimeout(() => this.scrolling ? waitScrollEnd(callback) : callback(), 100)
53 | }
54 | waitScrollEnd(() => resolve(true))
55 | })
56 | }
57 | }
58 |
59 | export {
60 | ModalityState
61 | }
62 |
--------------------------------------------------------------------------------
/src/Modality/View.ts:
--------------------------------------------------------------------------------
1 | import { ModalityEventTarget } from './EventTarget'
2 | import { SmoothScroller } from "../Scroll"
3 | import { setTimeout, testHasScrolling, getCSSUnits } from '../lib/util'
4 | import { getIOSversion } from '../lib/util/getIOSVersion'
5 | import { fullscreenBaseCSSText } from '../lib/cssText/fullscreenBaseCSSText'
6 | import { baseCSSText } from './cssText'
7 | import { Applet } from '../types'
8 |
9 | /**
10 | * Obsolete
11 | * ------------- start -------------
12 | */
13 | // old; ios < 9
14 | const NoScrolling = testHasScrolling() === false
15 | /**
16 | * Obsolete
17 | * ------------- end -------------
18 | */
19 | class ModalityView extends ModalityEventTarget {
20 | constructor(applet: Applet) {
21 | super(applet)
22 | this.scroller = new SmoothScroller(this.modalityContainer as HTMLElement)
23 | this.buildModalityOverlay()
24 | applet.viewport?.appendChild(this.modalityContainer)
25 | }
26 | private buildModalityOverlay() {
27 | const modalityOverlay = document.createElement('modality-overlay')
28 | modalityOverlay.style.cssText = `
29 | position: fixed;
30 | ${fullscreenBaseCSSText}
31 | z-index: 9;
32 | display: none;
33 | `
34 | modalityOverlay.addEventListener('touchstart', (event) => {
35 | // Prevent interruptions due to Alert, etc., from not closing the applet properly.
36 | if (!this.options?.stillBackdrop && this.degree <= this.advanceDegree && this.activity) {
37 | this.hide()
38 | }
39 | event.stopPropagation()
40 | event.preventDefault()
41 | }, true)
42 | this.modalityOverlay = modalityOverlay
43 | }
44 |
45 | public create(): HTMLElement {
46 | const viewport = this.appletViewport
47 | const modalityContainer = this.modalityContainer
48 | const containerFirstChild = modalityContainer.firstChild
49 | const modalityPlaceholder = document.createElement('modality-placeholder')
50 | const modalityHandle = document.createElement('modality-handle')
51 | const blockedHolder = document.createElement('blocked-holder')
52 | const contentContainer = document.createElement('applet-container')
53 | const modalityStyle = document.createElement('style')
54 | const options = this.options
55 | const hasMiniCard = !!options?.miniCardHeight && !NoScrolling
56 | const miniCardHeight = getCSSUnits(options?.miniCardHeight) || '0px'
57 | const blockedHolderWidth = getCSSUnits(options?.blockedHolderWidth) || '40px'
58 | const top = getCSSUnits(options?.top) || '60px'
59 | const maskClosable = options?.maskClosable
60 | const noHandlebar = options?.noHandlebar
61 | const borderRadius = getCSSUnits(options?.borderRadius) || '16px'
62 | modalityStyle.innerHTML = baseCSSText
63 | contentContainer.style.cssText = `
64 | position: relative;
65 | border: 0px;
66 | outline: 0px;
67 | min-width: 100%;
68 | min-height: calc(100% - ${top});
69 | max-height: calc(100% - ${top});
70 | border-top-left-radius: ${borderRadius};
71 | border-top-right-radius: ${borderRadius};
72 | scroll-snap-align: start;
73 | scroll-snap-stop: always;
74 | overflow: hidden;
75 | transform: translate3d(0, 0, 500px);
76 | `
77 | viewport.appendChild(modalityStyle)
78 | viewport.appendChild(blockedHolder)
79 | viewport.appendChild(this.modalityOverlay)
80 | modalityPlaceholder.appendChild(modalityHandle)
81 | // set blockedHolder width
82 | blockedHolder.style.width = blockedHolderWidth
83 | if (containerFirstChild) {
84 | modalityContainer.insertBefore(modalityPlaceholder, containerFirstChild)
85 | } else {
86 | modalityContainer.appendChild(modalityPlaceholder)
87 | }
88 | /**
89 | * Obsolete
90 | * ------------- start -------------
91 | */
92 | // ios < 12.55 bug
93 | if (NoScrolling) {
94 | modalityContainer.style.cssText += '-webkit-overflow-scrolling: touch;'
95 | }
96 | /**
97 | * Obsolete
98 | * ------------- end -------------
99 | */
100 | if (hasMiniCard) {
101 | const miniCard = document.createElement('modality-mini-card')
102 | miniCard.style.cssText = `
103 | width: 100%;
104 | min-height: calc(100% - ${miniCardHeight});
105 | scroll-snap-align: start;
106 | scroll-snap-stop: always;
107 | `
108 | modalityContainer.appendChild(miniCard)
109 | modalityHandle.style.top = `calc(200% - ${miniCardHeight})`
110 | this.miniCard = miniCard
111 | }
112 | if (maskClosable !== false) {
113 | modalityPlaceholder.addEventListener('click', () => {
114 | this.hide()
115 | })
116 | this.miniCard?.addEventListener('click', () => {
117 | this.hide()
118 | })
119 | }
120 | if (noHandlebar) {
121 | modalityHandle.style.display = 'none'
122 | }
123 |
124 | if (this.options?.swipeClosable ?? getIOSversion()) {
125 | setTimeout(() => {
126 | this.bindDragContentEvent()
127 | }, 10)
128 | }
129 | this.bindBaseEvent()
130 | modalityContainer.appendChild(contentContainer)
131 | this.modalityPlaceholder = modalityPlaceholder
132 | return this.contentContainer = contentContainer
133 | }
134 | }
135 |
136 | export {
137 | ModalityView
138 | }
139 |
--------------------------------------------------------------------------------
/src/Modality/cssText.ts:
--------------------------------------------------------------------------------
1 | import { coveredCSSText } from '../lib/cssText/coveredCSSText'
2 |
3 | export const baseCSSText = `
4 | applet-viewport {
5 | background: transparent !important;
6 | }
7 | modality-container {
8 | display: flex;
9 | height: 100%;
10 | overflow-y: scroll !important;
11 | scroll-behavior: smooth;
12 | scroll-snap-type: y mandatory;
13 | flex-direction: column;
14 | }
15 | modality-container::-webkit-scrollbar {
16 | display: none;
17 | }
18 | modality-container::scrollbar {
19 | display: none;
20 | }
21 | modality-placeholder {
22 | position: relative;
23 | display: block;
24 | ${coveredCSSText}
25 | scroll-snap-align: start;
26 | scroll-snap-stop: always;
27 | box-sizing: content-box;
28 | }
29 | modality-handle {
30 | display: block;
31 | position: absolute;
32 | top: 100%;
33 | height: 20px;
34 | width: 100%;
35 | z-index: 2;
36 | }
37 | modality-handle::before {
38 | content: ' ';
39 | display: block;
40 | width: 36px;
41 | height: 5px;
42 | margin: 5px auto;
43 | border-radius: 5px;
44 | background: #777;
45 | opacity: .5;
46 | }
47 | modality-handle:hover::before {
48 | opacity: 1;
49 | }
50 | blocked-holder {
51 | position: absolute;
52 | top: 200px;
53 | left: 0;
54 | bottom: 200px;
55 | width: 40px;
56 | z-index: 5;
57 | overflow-y: hidden;
58 | overflow-x: scroll;
59 | overscroll-behavior-y: none;
60 | display: flex;
61 | flex-flow: row-reverse;
62 | }
63 | blocked-holder::-webkit-scrollbar {
64 | display: none;
65 | }
66 | blocked-holder::scrollbar {
67 | display: none;
68 | }
69 | blocked-holder::before {
70 | content: ' ';
71 | display: flex;
72 | min-width: 100vw;
73 | height: 100%;
74 | }
75 | `
--------------------------------------------------------------------------------
/src/Modality/index.ts:
--------------------------------------------------------------------------------
1 | import { ModalityView } from "./View"
2 | class Modality extends ModalityView {
3 | }
4 |
5 | export {
6 | Modality
7 | }
8 |
--------------------------------------------------------------------------------
/src/Sandbox/index.ts:
--------------------------------------------------------------------------------
1 | class Sandbox {
2 | public sandbox: HTMLIFrameElement
3 | public setting?: string
4 | private readonly blankURL = 'about:blank'
5 | constructor(uri?: string, setting?: string, type: 'src' | 'source' = 'src') {
6 | const sandbox = this.sandbox = document.createElement('iframe')
7 | sandbox[type === 'source' && uri ? 'srcdoc' : 'src'] = uri || this.blankURL
8 | sandbox.style.display = 'none'
9 | this.setting = setting
10 | return this
11 | }
12 | get window() {
13 | return this.sandbox.contentWindow as Window
14 | }
15 | get document() {
16 | return this.sandbox.contentDocument as Document
17 | }
18 | get origin() {
19 | return this.src === this.blankURL ? null : this.src
20 | }
21 | set src(src: string) {
22 | this.sandbox.src = src
23 | }
24 | public setOnUnload(onunload: null | ((this: WindowEventHandlers, ev: Event) => unknown)) {
25 | try {
26 | this.window.onunload = onunload
27 | return Promise.resolve()
28 | } catch (error) {
29 | return Promise.reject(error)
30 | }
31 | }
32 | public setOnLoad(onload: (this: GlobalEventHandlers, ev: Event) => unknown) {
33 | try {
34 | this.sandbox.onload = onload
35 | return Promise.resolve()
36 | } catch (error) {
37 | return Promise.reject(error)
38 | }
39 | }
40 | public setOnError(onerror: OnErrorEventHandler) {
41 | try {
42 | this.sandbox.onerror = onerror
43 | return Promise.resolve()
44 | } catch (error) {
45 | return Promise.reject(error)
46 | }
47 | }
48 | public set(allow = this.setting): void {
49 | if (allow === undefined) return
50 | this.sandbox.setAttribute('sandbox', allow)
51 | }
52 | public reset(allow?: string): this {
53 | this.exit()
54 | this.set(allow)
55 | return this
56 | }
57 | public open(): this {
58 | this.document?.open()
59 | return this
60 | }
61 | public write(context = ''): this {
62 | context = '' + context
63 | this.document?.write(context)
64 | return this
65 | }
66 | public close(): this {
67 | this.document?.close()
68 | return this
69 | }
70 | public append(context: string | undefined): void {
71 | this.open()
72 | this.write(context)
73 | this.close()
74 | }
75 | public enter(container: HTMLElement): void {
76 | this.set()
77 | container.appendChild(this.sandbox)
78 | }
79 | public exit(): void {
80 | const parentNode = this.sandbox.parentNode as HTMLElement
81 | parentNode && parentNode.removeChild(this.sandbox)
82 | }
83 | }
84 |
85 | export {
86 | Sandbox
87 | }
88 |
--------------------------------------------------------------------------------
/src/Scroll/index.ts:
--------------------------------------------------------------------------------
1 | import { requestAnimationFrame as rAF, testHasSmoothScrolling, testHasSmoothSnapScrolling, setTimeout } from '../lib/util'
2 |
3 | interface StepOptions {
4 | startTime: number
5 | startX: number
6 | startY: number
7 | x: number
8 | y: number
9 | duration: number
10 | end: () => void
11 | }
12 |
13 | const now = window.performance && window.performance.now
14 | ? window.performance.now.bind(window.performance) : Date.now
15 | const easeInOutCubic = (x: number): number => {
16 | return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
17 | }
18 | const hasSmoothScrolling = testHasSmoothScrolling()
19 | const hasSmoothSnapScrolling = testHasSmoothSnapScrolling()
20 | /**
21 | * Obsolete
22 | * ------------- start -------------
23 | */
24 | // safari pc
25 | const breakSmoothSnap = !hasSmoothScrolling && /Mac OS X [0-9][0-9]_/g.exec(navigator.userAgent)
26 | /**
27 | * Obsolete
28 | * ------------- end -------------
29 | */
30 |
31 | class SmoothScroller {
32 | public element: HTMLElement
33 | private touchHold = false
34 | private scrollType: 'scroll' | 'snap' = 'scroll'
35 | private scrollHold: (() => void) | null = null
36 | private defaultScrollBehavior!: string
37 | private defaultScrollSnapType!: string
38 | private scrolling = false
39 | private preStep?: Array
40 | private reservePlanId = -1
41 | private scrollDuration = 400
42 | constructor(element: HTMLElement, stoppable = false) {
43 | this.element = element
44 | if (!hasSmoothScrolling && stoppable) {
45 | element.addEventListener('touchstart', this.scrollStop, true)
46 | element.addEventListener('touchend', this.scrollContinue, true)
47 | element.addEventListener('touchcancel', this.scrollContinue, true)
48 | }
49 | }
50 | public async snapTo(x: number, y: number, duration?: number): Promise {
51 | this.touchHold = false
52 | this.scrollType = 'snap'
53 | return this.smoothScroll(x, y, duration)
54 | }
55 | public async scrollTo(x: number, y: number, duration?: number): Promise {
56 | this.touchHold = false
57 | this.scrollType = 'scroll'
58 | return this.smoothScroll(x, y, duration)
59 | }
60 | private scrollStop(): void {
61 | this.touchHold = true
62 | }
63 | private scrollContinue(): void {
64 | this.touchHold = false
65 | this.scrollHold?.()
66 | }
67 | private catchDefaultStyle(): void {
68 | if (this.scrolling === true) return
69 | const style = this.element.style
70 | this.defaultScrollBehavior = style.scrollBehavior || 'smooth'
71 | }
72 | private isBreak(): boolean {
73 | if ("ontouchend" in document) return false
74 | if (this.preStep !== undefined && (this.preStep[0] !== this.element.scrollLeft || this.preStep[1] !== this.element.scrollTop)) {
75 | this.scrollStop()
76 | return true
77 | }
78 | return false
79 | }
80 | private step(context: StepOptions): void {
81 | if (this.touchHold) {
82 | if (this.scrollType === 'scroll') {
83 | return context.end()
84 | } else {
85 | this.scrollHold = () => {
86 | this.step(context)
87 | }
88 | }
89 | }
90 | const time = now()
91 | // avoid elapsed times higher than one
92 | const elapsed = Math.min((time - context.startTime) / context.duration, 1)
93 | // apply easing to elapsed time
94 | const value = easeInOutCubic(elapsed)
95 |
96 | let currentX = context.startX + (context.x - context.startX) * value
97 | let currentY = context.startY + (context.y - context.startY) * value
98 |
99 | const directionX = context.x - context.startX > 0 ? 1 : -1
100 | const directionY = context.y - context.startY > 0 ? 1 : -1
101 |
102 | if (directionX === 1 && currentX > context.x) {
103 | currentX = context.x
104 | } else if (directionX === -1 && currentX < context.x) {
105 | currentX = context.x
106 | }
107 |
108 | if (directionY === 1 && currentY > context.y) {
109 | currentY = context.y
110 | } else if (directionY === -1 && currentY < context.y) {
111 | currentY = context.y
112 | }
113 |
114 | if (this.isBreak()) return
115 |
116 | /**
117 | * Obsolete
118 | * ------------- start -------------
119 | */
120 | // step.1 for ios < 10.2
121 | this.element.scrollLeft = currentX
122 | this.element.scrollTop = currentY
123 | /**
124 | * Obsolete
125 | * ------------- end -------------
126 | */
127 | // step.2 Note the order!
128 | this.element.scrollTo(currentX, currentY)
129 |
130 | this.preStep = [this.element.scrollLeft, this.element.scrollTop]
131 |
132 | // scroll more if we have not reached our destination
133 | if (currentX !== context.x || currentY !== context.y) {
134 | rAF(() => {
135 | this.step(context)
136 | })
137 | } else {
138 | context.end()
139 | this.scrollHold = null
140 | }
141 | }
142 | private openSmooth(): void {
143 | this.element.style.scrollBehavior = 'smooth'
144 | /**
145 | * Obsolete
146 | * ------------- start -------------
147 | */
148 | if (breakSmoothSnap) {
149 | this.element.style.scrollSnapType = this.defaultScrollSnapType
150 | }
151 | /**
152 | * Obsolete
153 | * ------------- end -------------
154 | */
155 | }
156 | private closeSmooth(): void {
157 | this.element.style.scrollBehavior = 'auto'
158 | /**
159 | * Obsolete
160 | * ------------- start -------------
161 | */
162 | if (breakSmoothSnap) {
163 | this.defaultScrollSnapType = this.element.style.scrollSnapType
164 | this.element.style.scrollSnapType = 'none'
165 | }
166 | /**
167 | * Obsolete
168 | * ------------- end -------------
169 | */
170 | }
171 | private beforeScrolling(): void {
172 | this.catchDefaultStyle()
173 | this.preStep = undefined
174 | this.scrolling = true
175 | }
176 | private endScrolling(): void {
177 | this.element.style.scrollBehavior = this.defaultScrollBehavior || 'smooth'
178 | this.scrolling = false
179 | }
180 | private checkScrollEnd = (x: number, y: number): boolean => {
181 | return this.element.scrollLeft === x && this.element.scrollTop === y
182 | }
183 | private smoothScroll(x: number, y: number, duration?: number): Promise {
184 | if ((this.scrollType === 'scroll' && !hasSmoothScrolling) || (this.scrollType === 'snap' && !hasSmoothSnapScrolling)) {
185 | return this.smoothScrollByStep(x, y)
186 | }
187 | let scrollCycleTimeout = -1
188 | return new Promise((resolve) => {
189 | const scrollHandel = () => {
190 | clearTimeout(scrollCycleTimeout)
191 | scrollCycleTimeout = setTimeout(async () => {
192 | this.element.removeEventListener('scroll', scrollHandel, false)
193 | resolve()
194 | }, 100) as unknown as number
195 | }
196 | this.element.addEventListener('scroll', scrollHandel, false)
197 | this.beforeScrolling()
198 | this.openSmooth()
199 | if (this.checkScrollEnd(x, y)) {
200 | this.endScrolling()
201 | resolve()
202 | }
203 | this.element.scrollTo(x, y)
204 |
205 | clearTimeout(this.reservePlanId)
206 | // doesn't trigger scrolling
207 | this.reservePlanId = setTimeout(async () => {
208 | if (scrollCycleTimeout === -1) {
209 | await this.smoothScrollByStep(x, y, duration)
210 | resolve()
211 | }
212 | }, 600) as unknown as number
213 | })
214 | }
215 | private smoothScrollByStep(x: number, y: number, duration = this.scrollDuration): Promise {
216 | this.beforeScrolling()
217 | this.closeSmooth()
218 | return new Promise(resolve => {
219 | this.step({
220 | startTime: now(),
221 | startX: this.element.scrollLeft,
222 | startY: this.element.scrollTop,
223 | x: x,
224 | y: y,
225 | duration,
226 | end: () => {
227 | this.endScrolling()
228 | resolve()
229 | }
230 | })
231 | })
232 | }
233 | }
234 |
235 | export {
236 | SmoothScroller
237 | }
--------------------------------------------------------------------------------
/src/Scroll/polyfill.ts:
--------------------------------------------------------------------------------
1 | export default (): Promise => {
2 | if (typeof Element.prototype.scrollTo !== 'function') {
3 | return import('scroll-polyfill/auto').catch((e) => {
4 | console.warn(e)
5 | })
6 | }
7 | return Promise.resolve()
8 | }
9 |
--------------------------------------------------------------------------------
/src/Scroll/types.module.ts:
--------------------------------------------------------------------------------
1 | declare module 'scroll-polyfill/auto'
--------------------------------------------------------------------------------
/src/Segue/Animation.ts:
--------------------------------------------------------------------------------
1 | import { SegueState } from './State'
2 | import typeError from '../lib/typeError'
3 | import { SegueAnimateState, AnimationConfig, AnimationPrestType, Applet } from '../types'
4 |
5 | type SegueAnimateFn = (e: SegueAnimateState) => undefined | Promise
6 |
7 | class SegueAnimation extends SegueState {
8 | public checkAnimationNamed(): boolean {
9 | return !!this.getAnimationNames()
10 | }
11 |
12 | public checkNativeAnimation(): boolean {
13 | const animationNames = this.getAnimationNames()
14 | if (typeof animationNames === 'string') {
15 | return ['slide', 'slide-left'].includes(animationNames)
16 | }
17 | return false
18 | }
19 |
20 | public getAnimationNames(): AnimationPrestType | boolean | undefined | AnimationConfig {
21 | if (this.options.index && this.applet.isFullscreen && this.isEntryApplet) return false
22 | if (this.fromOverscrollHistoryNavigation || this.backFromType === 'controls') return false
23 | const usePrevAppletAnimation = this.countercurrent
24 | const animationNames = this[usePrevAppletAnimation ? 'prevApplet' : 'applet']?.config.animation ?? 'slide'
25 | if (animationNames === true || animationNames === 'inherit') {
26 | return this.options.defaultAnimation
27 | }
28 | return animationNames
29 | }
30 |
31 | public async getAnimationGroup(): Promise<[SegueAnimateFn, SegueAnimateFn] | SegueAnimateFn | undefined> {
32 | let animationFunction: [SegueAnimateFn, SegueAnimateFn] | SegueAnimateFn | undefined
33 | const animationNames = this.getAnimationNames()
34 | if (typeof animationNames === 'string') {
35 | animationFunction = await this.getAnimationByName(animationNames)
36 | } else if (Array.isArray(animationFunction) && typeof (animationNames as [SegueAnimateFn, SegueAnimateFn])[0] !== 'function') {
37 | return
38 | }
39 |
40 | return animationFunction
41 | }
42 |
43 | public async getAnimationOneSide(backset: number): Promise {
44 | const animationGroup = await this.getAnimationGroup()
45 | if (backset >= 0) {
46 | switch (typeof animationGroup) {
47 | case 'function':
48 | return animationGroup
49 | case 'object':
50 | return animationGroup[animationGroup.length === 2 ? backset : 0]
51 | default:
52 | return
53 | }
54 | }
55 | return
56 | }
57 |
58 | public preloadAnimation(applet: Applet) {
59 | const animationNames = applet?.config?.animation ?? 'slide'
60 | this.getAnimationByName(animationNames as AnimationPrestType).catch(() => {
61 | // Load trunk error
62 | // console.warn(`Load animation[${animationNames}] trunk error!`)
63 | })
64 | }
65 |
66 | public async getAnimationByName(type: AnimationPrestType): Promise<[SegueAnimateFn, SegueAnimateFn] | SegueAnimateFn | undefined> {
67 | switch (type) {
68 | case 'popup':
69 | return new Promise((resolve, reject) => {
70 | import('./preset/popup').then((animate) => {
71 | const popup = animate.default
72 | resolve([popup, popup])
73 | }).catch(reject)
74 | })
75 | case 'grow':
76 | return new Promise((resolve, reject) => {
77 | import('./preset/grow').then((animate) => {
78 | const grow = animate.default
79 | resolve(grow)
80 | }).catch(reject)
81 | })
82 | case 'flip':
83 | return new Promise((resolve, reject) => {
84 | import('./preset/flip').then((animate) => {
85 | const flip = animate.default
86 | resolve([flip(4), flip(4)])
87 | }).catch(reject)
88 | })
89 | case 'flip-left':
90 | return new Promise((resolve, reject) => {
91 | import('./preset/flip').then((animate) => {
92 | const flip = animate.default
93 | resolve([flip(3), flip(3)])
94 | }).catch(reject)
95 | })
96 | case 'flip-down':
97 | return new Promise((resolve, reject) => {
98 | import('./preset/flip').then((animate) => {
99 | const flip = animate.default
100 | resolve([flip(2), flip(2)])
101 | }).catch(reject)
102 | })
103 | case 'flip-right':
104 | return new Promise((resolve, reject) => {
105 | import('./preset/flip').then((animate) => {
106 | const flip = animate.default
107 | resolve([flip(1), flip(1)])
108 | }).catch(reject)
109 | })
110 | case 'flip-up':
111 | return new Promise((resolve, reject) => {
112 | import('./preset/flip').then((animate) => {
113 | const flip = animate.default
114 | resolve([flip(0), flip(0)])
115 | }).catch(reject)
116 | })
117 | case 'fade':
118 | return new Promise((resolve, reject) => {
119 | import('./preset/fade').then((animate) => {
120 | const fade = animate.default
121 | resolve([fade(1), fade(0)])
122 | }).catch(reject)
123 | })
124 | case 'zoom':
125 | return new Promise((resolve, reject) => {
126 | import('./preset/zoom').then((animate) => {
127 | const zoom = animate.default
128 | resolve([zoom(1), zoom(0)])
129 | }).catch(reject)
130 | })
131 | case 'slide-right':
132 | return new Promise((resolve, reject) => {
133 | import('./preset/slide').then((animate) => {
134 | const slide = animate.default
135 | resolve([slide(3), slide(1)])
136 | }).catch(reject)
137 | })
138 | case 'slide-up':
139 | return new Promise((resolve, reject) => {
140 | import('./preset/slide').then((animate) => {
141 | const slide = animate.default
142 | resolve([slide(2), slide(0)])
143 | }).catch(reject)
144 | })
145 | case 'slide-down':
146 | return new Promise((resolve, reject) => {
147 | import('./preset/slide').then((animate) => {
148 | const slide = animate.default
149 | resolve([slide(0), slide(2)])
150 | }).catch(reject)
151 | })
152 | case 'slide':
153 | case 'slide-left':
154 | default:
155 | // use preload
156 | if (!this.applet || !this.options) {
157 | return
158 | }
159 | if (this.applet.config.modality) {
160 | typeError(1007)
161 | return
162 | }
163 | return new Promise((resolve, reject) => {
164 | if (this.options.swipeModel) {
165 | import('./preset/slide-native').then((animate) => {
166 | const slide = animate.default
167 | resolve([slide, slide])
168 | }).catch(reject)
169 | } else {
170 | import('./preset/slide').then((animate) => {
171 | const slide = animate.default
172 | resolve([slide(1), slide(3)])
173 | }).catch(reject)
174 | }
175 | })
176 | }
177 | }
178 | }
179 |
180 | export {
181 | SegueAnimation
182 | }
183 |
--------------------------------------------------------------------------------
/src/Segue/Base.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '../Application'
2 | import { Applet } from '../Applet'
3 | import { fullscreenBaseCSSText } from '../lib/cssText/fullscreenBaseCSSText'
4 | import { SegueOptions, SegueActionOrigin, PresetConfig } from '../types'
5 |
6 | class SegueBase {
7 | public id = ''
8 | public prevId = ''
9 | public param = ''
10 | public root: HTMLElement | ShadowRoot
11 | public application: Application
12 | public zIndex: number
13 | public applet!: Applet
14 | public prevApplet?: Applet
15 | public prevPrevApplet?: Applet
16 | public appletGroup!: Array