diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 7665882749..ed10c76942 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'vm/**' + - 'Ports/JavaScriptPort/**' - '!vm/**/README.md' - '!vm/**/readme.md' - '!vm/**/docs/**' @@ -11,6 +12,7 @@ on: branches: [ master, main ] paths: - 'vm/**' + - 'Ports/JavaScriptPort/**' - '!vm/**/README.md' - '!vm/**/readme.md' - '!vm/**/docs/**' @@ -79,6 +81,11 @@ jobs: java-version: '8' cache: 'maven' + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Run ParparVM JVM tests working-directory: vm run: mvn -B clean package -pl JavaAPI -am -DskipTests && mvn -B test -pl tests -am -DexcludedGroups=benchmark diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index eeb5d789e7..0eae5fbac0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -72,6 +72,11 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2 + - name: Set up Node + if: ${{ matrix.java-version == 8 }} + uses: actions/setup-node@v4 + with: + node-version: '20' - name: Run Unit Tests run: | MVN_ARGS="" @@ -110,6 +115,10 @@ jobs: - name: Run SpotBugs for ByteCodeTranslator if: ${{ matrix.java-version == 8 }} run: mvn -B -DskipTests=true -f vm/ByteCodeTranslator/pom.xml verify + - name: Run JavaScript Port smoke integration + if: ${{ matrix.java-version == 8 }} + working-directory: vm + run: mvn -B test -pl tests -am -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest=JavaScriptPortSmokeIntegrationTest - name: Generate static analysis HTML summaries if: ${{ always() && matrix.java-version == 8 }} env: diff --git a/Ports/JavaScriptPort/LICENSE.md b/Ports/JavaScriptPort/LICENSE.md new file mode 100644 index 0000000000..9256be8701 --- /dev/null +++ b/Ports/JavaScriptPort/LICENSE.md @@ -0,0 +1,6 @@ +JavaScriptPort and its dedicated fixtures are licensed under the PolyForm Noncommercial License 1.0.0. + +Official license text: +[https://polyformproject.org/licenses/noncommercial/1.0.0/](https://polyformproject.org/licenses/noncommercial/1.0.0/) + +This license boundary applies to files inside `Ports/JavaScriptPort/**` unless a file states otherwise. diff --git a/Ports/JavaScriptPort/NOTICE.md b/Ports/JavaScriptPort/NOTICE.md new file mode 100644 index 0000000000..7eefaaa9c8 --- /dev/null +++ b/Ports/JavaScriptPort/NOTICE.md @@ -0,0 +1,3 @@ +This subtree is intentionally licensed separately from the parent Codename One repository. + +Files in `Ports/JavaScriptPort/**` are licensed under PolyForm Noncommercial 1.0.0 and must not inherit the parent repository's standard source-file license headers. diff --git a/Ports/JavaScriptPort/README.md b/Ports/JavaScriptPort/README.md new file mode 100644 index 0000000000..f90280f066 --- /dev/null +++ b/Ports/JavaScriptPort/README.md @@ -0,0 +1,12 @@ +JavaScriptPort is the browser-runtime work area for the new Codename One JavaScript port. + +Scope of this bootstrap: +- imported HTML5/runtime code from the existing proprietary browser port as the behavioral/source baseline +- PolyForm-licensed port boundary under `Ports/JavaScriptPort/**` +- ParparVM-oriented smoke fixtures under `Ports/JavaScriptPort/tests/**` +- executable translator/runtime coverage through the local ParparVM test suite in `vm/tests` + +License boundary: +- `Ports/JavaScriptPort/**` is licensed under PolyForm Noncommercial 1.0.0 +- dedicated smoke fixtures under `Ports/JavaScriptPort/tests/**` use the same license boundary +- the rest of the repository remains under its existing licensing diff --git a/Ports/JavaScriptPort/STATUS.md b/Ports/JavaScriptPort/STATUS.md new file mode 100644 index 0000000000..448b304894 --- /dev/null +++ b/Ports/JavaScriptPort/STATUS.md @@ -0,0 +1,51 @@ +JavaScript Port Status +====================== + +Implemented +----------- + +- [x] PolyForm Noncommercial 1.0.0 license boundary for `Ports/JavaScriptPort/**` +- [x] Imported browser-port baseline into the repository as a working reference subtree +- [x] ParparVM-side production host bridge in [JavaScriptPortHost.java](/Users/shai/dev/cn1/Ports/JavaScriptPort/src/main/java/com/codename1/impl/platform/js/JavaScriptPortHost.java) +- [x] ParparVM translator registration for JavaScript-port host natives in [JavascriptNativeRegistry.java](/Users/shai/dev/cn1/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptNativeRegistry.java) +- [x] Browser bundle bootstrap shell and host bridge in `vm/ByteCodeTranslator` +- [x] PolyForm smoke fixtures for the JavaScript port under `Ports/JavaScriptPort/tests/**` +- [x] ParparVM smoke and browser-bundle integration coverage in `vm/tests` +- [x] Extraction of reusable runtime helpers from `HTML5Implementation` +- [x] Extraction of input/bootstrap/event wiring helpers from `HTML5Implementation` +- [x] Initial rendering subsystem extraction from `HTML5Graphics` and `BufferedGraphics` +- [x] Shared native-image model and cache invalidation policy via [JavaScriptNativeImageAdapter.java](/Users/shai/dev/cn1/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptNativeImageAdapter.java) +- [x] Async image load state coordination via [JavaScriptAsyncImageLoadCoordinator.java](/Users/shai/dev/cn1/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptAsyncImageLoadCoordinator.java) +- [x] Rendering backend contract in [JavaScriptRenderingBackend.java](/Users/shai/dev/cn1/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JavaScriptRenderingBackend.java) +- [x] Neutral default backend implementation name: `BrowserDomRenderingBackend` +- [x] Backend-routed image creation, scaling, pixel readback, and canvas serialization for the main image runtime paths + +In Progress +----------- + +- [ ] Replace remaining direct browser-runtime assumptions in `HTML5Implementation` with backend-owned or adapter-owned seams +- [ ] Reduce the remaining `org.teavm.*` and `com.codename1.teavm.*` coupling inside the production runtime path +- [ ] Define a concrete ParparVM-native browser/runtime backend implementation behind `JavaScriptRenderingBackend` + +TODO +---- + +- [ ] Implement a non-TeaVM production backend behind `JavaScriptRenderingBackend` +- [ ] Port the remaining image/media/photo-capture canvas paths onto backend-owned abstractions where appropriate +- [ ] Port browser peer integration and native peer lifecycle beyond the current extraction layer +- [ ] Port text editing and native text-field overlay behavior away from legacy assumptions +- [ ] Port networking and storage from extracted helper contracts into a clearly ParparVM-oriented runtime path +- [ ] Port browser/media/database/file-chooser integrations away from direct legacy dependencies +- [ ] Replace or adapt legacy `teavm` package names in production code where they are part of the long-term runtime contract +- [ ] Expand runtime tests from smoke/contract coverage into broader end-to-end JavaScript-port behavior coverage +- [ ] Add a dedicated implementation-factory selection path for the JavaScript port if the final runtime wiring needs one beyond current bootstrap behavior + +Verification +------------ + +- Focused verification currently uses: + ```bash + export JAVA_HOME=/Users/shai/Library/Java/JavaVirtualMachines/azul-1.8.0_372/Contents/Home + export PATH="$JAVA_HOME/bin:$PATH" + mvn -B test -f /Users/shai/dev/cn1/vm/pom.xml -pl tests -am -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest=JavaScriptRuntimeFacadeTest,JavaScriptPortSmokeIntegrationTest,JavascriptTargetIntegrationTest#generatesBrowserBundleForJavascriptTarget + ``` diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java new file mode 100644 index 0000000000..39afa0fb5c --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ +package com.codename1.impl; + +import com.codename1.impl.html5.HTML5Implementation; +import com.codename1.impl.html5.JavaScriptPortBootstrap; +import com.codename1.impl.html5.URLProxifier; +import com.codename1.ui.Display; + +public class ImplementationFactory { + private static ImplementationFactory instance = new ImplementationFactory(); + + protected ImplementationFactory() { + } + + public static ImplementationFactory getInstance() { + return instance; + } + + public static void setInstance(ImplementationFactory factory) { + instance = factory; + } + + public Object createImplementation() { + HTML5Implementation implementation = new HTML5Implementation(); + implementation.setUrlProxifier(new URLProxifier() { + @Override + public String proxifyURL(String url) { + return JavaScriptPortBootstrap.proxifyUrl(Display.getInstance(), url); + } + }); + return implementation; + } +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java new file mode 100644 index 0000000000..c5aded6ea9 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.impl.html5.HTML5Implementation.NativeFont; +import com.codename1.impl.html5.HTML5Implementation.NativeImage; +import com.codename1.impl.html5.graphics.ClearRect; +import com.codename1.impl.html5.graphics.ClipRect; +import com.codename1.impl.html5.graphics.ClipShape; +import com.codename1.impl.html5.graphics.DrawArc; +import com.codename1.impl.html5.graphics.DrawImage; +import com.codename1.impl.html5.graphics.DrawLine; +import com.codename1.impl.html5.graphics.DrawPolygon; +import com.codename1.impl.html5.graphics.DrawRect; +import com.codename1.impl.html5.graphics.DrawRoundRect; +import com.codename1.impl.html5.graphics.DrawShape; +import com.codename1.impl.html5.graphics.DrawString; +import com.codename1.impl.html5.graphics.ExecutableOp; +import com.codename1.impl.html5.graphics.FillArc; +import com.codename1.impl.html5.graphics.FillLinearGradient; +import com.codename1.impl.html5.graphics.FillPolygon; +import com.codename1.impl.html5.graphics.FillRadialGradient; +import com.codename1.impl.html5.graphics.FillRect; +import com.codename1.impl.html5.graphics.FillRoundRect; +import com.codename1.impl.html5.graphics.FillShape; +import com.codename1.impl.html5.graphics.SetTransform; +import com.codename1.impl.html5.graphics.TileImage; +import com.codename1.teavm.geom.JSAffineTransform; +import com.codename1.ui.Stroke; +import com.codename1.ui.Transform; +import com.codename1.ui.geom.GeneralPath; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.geom.Shape; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.dom.html.HTMLCanvasElement; + +/** + * + * @author shannah + */ +public class BufferedGraphics extends HTML5Graphics { + + Queue upcoming = new LinkedList(); + private Rectangle clipRect; + private Rectangle clip=new Rectangle(); + private Rectangle clipBounds=new Rectangle(); + private boolean clipBoundsDirty=true; + private GeneralPath clipShape = new GeneralPath(); + + private boolean isClipShape; + private Transform transform, clipTransform; + private boolean transformApplied=false; + private final JavaScriptPrimitiveRenderAdapter primitiveRenderAdapter = + new JavaScriptPrimitiveRenderAdapter(getRenderState(), + new JavaScriptPrimitiveRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + upcoming.add(operation); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + private final JavaScriptImageTransformRenderAdapter imageTransformRenderAdapter = + new JavaScriptImageTransformRenderAdapter(getRenderState(), + new JavaScriptImageTransformRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + upcoming.add(operation); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + private final JavaScriptShapeGradientRenderAdapter shapeGradientRenderAdapter = + new JavaScriptShapeGradientRenderAdapter(getRenderState(), + new JavaScriptShapeGradientRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + upcoming.add(operation); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + + public BufferedGraphics(HTML5Implementation impl, HTMLCanvasElement canvas) { + super(impl, canvas); + } + + @Override + public void drawImage(Object img, int x, int y) { + imageTransformRenderAdapter.drawImage((NativeImage)img, x, y); + } + + @Override + public void drawImage(Object img, int x, int y, int w, int h) { + imageTransformRenderAdapter.drawImage((NativeImage)img, x, y, w, h); + } + + @Override + public void tileImage(Object img, int x, int y, int w, int h) { + imageTransformRenderAdapter.tileImage((NativeImage)img, x, y, w, h); + } + + + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + upcoming.add(new DrawArc(x, y, width, height, startAngle, arcAngle, getColor(), getAlpha())); + } + + @Override + public void fillRect(int x, int y, int width, int height) { + primitiveRenderAdapter.fillRect(x, y, width, height); + } + + @Override + public void clearRect(int x, int y, int width, int height) { + primitiveRenderAdapter.clearRect(x, y, width, height); + } + + + + @Override + public void drawRect(int x, int y, int width, int height) { + primitiveRenderAdapter.drawRect(x, y, width, height); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + primitiveRenderAdapter.drawLine(x1, y1, x2, y2); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + upcoming.add(new DrawRoundRect(x, y, width, height, arcWidth, arcHeight, getColor(), getAlpha())); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + upcoming.add(new FillRoundRect(x, y, width, height, arcWidth, arcHeight, getColor(), getAlpha())); + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + upcoming.add(new DrawPolygon(xPoints, yPoints, nPoints, getColor(), getAlpha())); + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + upcoming.add(new FillPolygon(xPoints, yPoints, nPoints, getColor(), getAlpha())); + } + + @Override + public void drawShape(Shape shape, Stroke stroke) { + shapeGradientRenderAdapter.drawShape(shape, stroke); + } + + @Override + public void fillShape(Shape shape) { + shapeGradientRenderAdapter.fillShape(shape); + } + + @Override + public void setTransform(Transform t) { + setTransform(t, true); + } + + @Override + public void setTransform(Transform t, boolean replace) { + if (transform == null || replace) { + transform = t; + } else if (!replace) { + transform.concatenate(t); + } + setTransformChanged(); + applyTransform(); + } + + @Override + public void applyTransform() { + if (!transformApplied) { + imageTransformRenderAdapter.applyTransform(((JSAffineTransform)transform.getNativeTransform()).cloneTransform(), true); + transformApplied = true; + } + } + + @Override + public void setTransformChanged() { + transformApplied = false; + clipBoundsDirty = true; + } + + @Override + public Transform getTransform() { + if (transform == null) { + transform = Transform.makeIdentity(); + } + return transform; + } + + @Override + public void resetAffine() { + if (transform != null && !transform.isIdentity()) { + transform.setIdentity(); + setTransformChanged(); + applyTransform(); + } + } + + @JSBody(params={"str"}, script="console.log(str)") + private native static void log(String str); + + + @JSBody(params={"str"}, script="console.log(str)") + private native static void log(JSObject str); + + @Override + public void rotate(double angle) { + if (transform == null) transform = Transform.makeIdentity(); + transform.rotate((float)angle, 0, 0); + setTransformChanged(); + applyTransform(); + } + + @Override + public void rotate(double angle, int pivotX, int pivotY) { + if (transform == null) transform = Transform.makeIdentity(); + transform.rotate((float)angle, pivotX, pivotY); + setTransformChanged(); + applyTransform(); + } + + @Override + public void scale(double sx, double sy) { + if (transform == null) transform = Transform.makeIdentity(); + transform.scale((float)sx, (float)sy); + setTransformChanged(); + applyTransform(); + } + + //@Override + //public void shear(double shx, double shy) { + // setTransform(JSAffineTransform.Factory.getShearInstance(shx, shy), false); + //} + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + upcoming.add(new FillArc(x, y, width, height, startAngle, arcAngle, getColor(), getAlpha())); + } + + @Override + public void drawRGB(int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { + if (offset != 0){ + int[] copy = new int[w*h]; + System.arraycopy(rgbData, offset, copy, 0, w*h); + rgbData = copy; + } + NativeImage img = (NativeImage)impl.createImage(rgbData, w, h); + drawImage(img, x, y, w, h); + + } + + @Override + public void drawString(String str, int x, int y) { + primitiveRenderAdapter.drawString(str, x, y); + } + + @Override + void setAlpha(int alpha) { + getRenderState().setAlpha(alpha); + } + + @Override + void setColor(int color) { + getRenderState().setColor(color); + } + + @Override + void setFont(NativeFont font) { + getRenderState().setFont(font); + } + + List flush(int x, int y, int width, int height){ + List current; + synchronized(upcoming){ + current = new ArrayList(upcoming.size()); + current.addAll(upcoming); + upcoming.clear(); + } + + return current; + } + + private Transform getInverseTransform() { + if (transform == null) return null; + return transform.getInverse(); + } + + private Shape getCurrentClipProjection() { + if (isClipShape) { + GeneralPath out = new GeneralPath(clipShape); + Transform t = Transform.makeIdentity(); + if (clipTransform != null && !clipTransform.isIdentity()) { + t.concatenate(clipTransform); + } + if (transform != null && !transform.isIdentity()) { + t.concatenate(transform.getInverse()); + } + if (!t.isIdentity()) { + out.transform(t); + } + return out; + } else { + if (transform != null && !transform.isIdentity()) { + GeneralPath out = new GeneralPath(); + out.setRect(clip, transform.getInverse()); + return out; + } else { + return clip; + } + } + } + + @Override + public void setClip(Shape shape) { + clipShape.reset(); + clipShape.setShape(shape, null); + isClipShape = true; + clipTransform = transform == null ? null : transform.copy(); + JSAffineTransform t = null; + if (transform != null) { + t = (JSAffineTransform)transform.getNativeTransform(); + } + clipBoundsDirty = true; + imageTransformRenderAdapter.setClipShape(shape, t); + } + + private void clipShape(Shape shape) { + if (!isClipShape) { + isClipShape = true; + clipShape.reset(); + clipShape.setShape(clip, null); + clipTransform = null; + } + GeneralPath p = (GeneralPath)getCurrentClipProjection(); + p.intersect(shape); + setClip(p); + } + + + @Override + public void setClip(int x, int y, int width, int height) { + if (transform != null && !transform.isIdentity()) { + setClip(new Rectangle(x, y, width, height)); + return; + } + isClipShape = false; + clip.setBounds(x, y, width, height); + clipBoundsDirty = true; + primitiveRenderAdapter.setClipRect(x, y, width, height); + } + + @Override + public void clipRect(int x, int y, int width, int height) { + Rectangle rect = new Rectangle(x, y, width, height); + if (isClipShape || transform != null && !transform.isIdentity()) { + clipShape(rect); + return; + } + + if (rect.contains(clip)) { + return; + } + clip = clip.intersection(x, y, width, height); + clipBoundsDirty = true; + primitiveRenderAdapter.setClipRect(clip.getX(), clip.getY(), clip.getWidth(), clip.getHeight()); + } + + private void calculateClipBounds() { + if (clipBoundsDirty) { + clipBoundsDirty = false; + Rectangle projectedShape = getCurrentClipProjection().getBounds(); + clipBounds.setBounds(projectedShape.getX(), projectedShape.getY(), projectedShape.getWidth(), projectedShape.getHeight()); + } + } + + @Override + public int getClipHeight() { + calculateClipBounds(); + return clipBounds.getHeight(); + } + + @Override + public int getClipWidth() { + calculateClipBounds(); + return clipBounds.getWidth(); + } + + @Override + public int getClipX() { + calculateClipBounds(); + return clipBounds.getX(); + } + + @Override + public int getClipY() { + calculateClipBounds(); + return clipBounds.getY(); + } + + + @Override + public void fillLinearGradient(int x, int y, int width, int height, int startColor, int endColor, boolean horizontal) { + shapeGradientRenderAdapter.fillLinearGradient(x, y, width, height, startColor, endColor, horizontal); + } + + @Override + public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height, int startAngle, int arcAngle) { + shapeGradientRenderAdapter.fillRadialGradient(x, y, width, height, startColor, endColor, startAngle, arcAngle); + } + + @Override + public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height) { + shapeGradientRenderAdapter.fillRadialGradient(x, y, width, height, startColor, endColor, 0, 360); + } + + @Override + public int getAlpha() { + return getRenderState().getAlpha(); + } + + @Override + public int getColor() { + return getRenderState().getColor(); + } + + @Override + public NativeFont getFont() { + return getRenderState().getFont(); + } + + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/FileChooser.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/FileChooser.java new file mode 100644 index 0000000000..325908c15c --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/FileChooser.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import org.teavm.jso.JSBody; +import org.teavm.jso.dom.html.HTMLInputElement; + +/** + * + * @author shannah + */ +public class FileChooser { + + @JSBody(params={"types"}, script="return jQuery('').get(0)") + private native static HTMLInputElement createFileInput(String[] types); + + public String openDialog(String[] allowedTypes) { + throw new UnsupportedOperationException("openDialog not implemented yet"); + } + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java new file mode 100644 index 0000000000..60208f7a2b --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserComponent.java @@ -0,0 +1,864 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.impl.html5.HTML5Implementation.TouchEvent; +import com.codename1.impl.html5.JSOImplementations.DocumentExt; +import com.codename1.impl.html5.JSOImplementations.HTMLIFrameElement; +import com.codename1.impl.html5.JSOImplementations.WindowExt; +import com.codename1.io.ConnectionRequest; +import com.codename1.io.Log; +import com.codename1.io.NetworkEvent; +import com.codename1.io.NetworkManager; +import com.codename1.teavm.jso.util.JS; +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.CN; +import com.codename1.ui.Display; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.sun.org.apache.xml.internal.serializer.ToHTMLStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.UIManager; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.css.CSSStyleDeclaration; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.events.MessageEvent; +import org.teavm.jso.dom.events.MouseEvent; +import org.teavm.jso.dom.html.HTMLDocument; +import org.teavm.jso.dom.html.HTMLElement; +import org.teavm.jso.dom.html.TextRectangle; + +/** + * + * @author shannah + */ +public class HTML5BrowserComponent extends HTML5Peer { + + BrowserComponent parent; + HTMLIFrameElement iframe; + Map properties = new HashMap(); + private boolean isAdded = false; + private boolean isCORSRestricted = false; + private static int nextId = 1; + private int id; + private boolean useProxy; + private boolean proxyInitialized; + private HTML5Proxy proxy; + private String proxyUrl; + private ArrayList proxyHistory; + private int proxyHistoryPos = -1; + private boolean doNotAddToHistory = false; + private EventListener messageListener = new EventListener() { + + + @Override + public void handleEvent(final MessageEvent e) { + Window win = iframe == null ? Window.current() : iframe.getContentWindow(); + //HTML5Implementation._log("Received event "+e.getDataAsString()); + if (getEventSource(e) == win) { + //HTML5Implementation._log("From our iframe"); + HTML5Implementation.callSerially(new Runnable() { + public void run() { + parent.fireWebEvent(BrowserComponent.onMessage, new ActionEvent(e.getDataAsString())); + + } + }); + } else { + //HTML5Implementation._log("Not from our iframe"); + } + + } + + }; + + @JSBody(params="evt", script="return evt.source") + private native static JSObject getEventSource(Event evt); + + @JSBody(params={"type", "bubbles", "cancelable"}, script="return new CustomEvent(type, {bubbles:bubbles, cancelable:cancelable})") + private native static CustomEvent newCustomEvent(String type, boolean bubbles, boolean cancelable); + + @JSBody(params={"event", "iframe", "x", "y"}, script="return window.copyTouchEvent(event, iframe, x, y)") + public native static TouchEvent copyTouchEvent(Event event, HTMLIFrameElement iframe, int x, int y); + + @JSBody(params={"event", "iframe", "x", "y"}, script="return window.copyWheelEvent(event, iframe, x, y)") + public native static Event copyWheelEvent(Event event, HTMLIFrameElement iframe, int x, int y); + + + @JSBody(params={"iframe", "message", "targetOrigin"}, script="iframe.contentWindow.postMessage(message, targetOrigin);") + private native static void _postMessage(HTMLIFrameElement iframe, String message, String targetOrigin); + + + + public void postMessage(String message, String targetOrigin) { + if (iframe != null) { + _postMessage(iframe, message, targetOrigin); + } + } + + private boolean messageListenerInstalled; + public void installMessageListener() { + if (iframe == null) { + return; + } + if (!messageListenerInstalled) { + messageListenerInstalled = true; + //HTML5Implementation._log("Installing message listener"); + Window.current().addEventListener("message", messageListener, false); + } + + } + + public void uninstallMessageListener() { + if (messageListenerInstalled) { + messageListenerInstalled = false; + Window.current().removeEventListener("message", messageListener, false); + } + + } + + + + private static interface CustomEvent extends Event { + @JSProperty + public void setClientX(int x); + + @JSProperty + public void setClientY(int y); + } + + private boolean cancelScroll; + + + + + private EventListener eventRouter = new EventListener() { + @Override + public void handleEvent(Event event) { + String eventType = event.getType(); + //HTML5Implementation._loSg("Routing event"); + //HTML5Implementation._log(event.getType()); + if (iframe == null) { + return; + } + TextRectangle clRect = iframe.getBoundingClientRect(); + Event evt; + if ("MozMousePixelScroll".equals(eventType) || eventType.equals(HTML5Implementation.getWheelEventType())) { + evt = copyWheelEvent(event, iframe, clRect.getLeft(), clRect.getTop()); + } else if (eventType.startsWith("mouse") || eventType.startsWith("pointer") + // On parts of the canvas painted over top of the iframe, we don't seem to + // get mouse events *but* we *do* get pointer events, so we'll use + // these events to pass along to the peers container so that + // CN1 can process these events. FFS + || (eventType.equals("pointerdown") || eventType.equals("pointerup"))) { + + MouseEvent mevt = (MouseEvent)event; + CustomEvent cevt = newCustomEvent(eventType, true, true); + cevt.setClientX(mevt.getClientX() + clRect.getLeft()); + cevt.setClientY(mevt.getClientY() + clRect.getTop()); + evt = cevt; + + } else if (eventType.startsWith("touch")) { + TouchEvent tevt = copyTouchEvent(event, iframe, clRect.getLeft(), clRect.getTop()); + evt = tevt; + } else if (eventType.startsWith("pointer")){ + MouseEvent mevt = (MouseEvent)event; + CustomEvent cevt = newCustomEvent("hittest", true, true); + cevt.setClientX(mevt.getClientX() + clRect.getLeft()); + cevt.setClientY(mevt.getClientY() + clRect.getTop()); + evt = cevt; + + } else { + return; + } + + HTMLElement targetEl = isFirefox() ? HTML5Implementation.getInstance().peersContainer : iframe; + if (!targetEl.dispatchEvent(evt)) { + + if ("touchmove".equals(eventType) || "pointermove".equals(eventType)) { + // On iOS we need to cancel scrolling of the iframe + // for touch events that are blocked by the CN1 UI + cancelScroll(); + } + event.preventDefault(); + event.stopPropagation(); + } else { + // Allow scrolling in case it wasn't allowed before + uncancelScroll(); + } + if ("touchend".equals(eventType) || "pointerup".equals(eventType)) { + // Touchend should always re-allow scrolling. + uncancelScroll(); + } + } + + }; + + + private void cancelScroll() { + if (iframe == null) { + return; + } + if (!cancelScroll) { + cancelScroll = true; + if (isIOSMobile()) { + // On iOS we cancel scrolling by preventing the parent div + // from scrolling. + HTMLElement el = el(); + el.getStyle().setProperty("overflow", "hidden"); + el.getStyle().removeProperty("-webkit-overflow-scrolling"); + } else { + iframe.getStyle().setProperty("pointer-events", "none"); + } + } + } + + private void uncancelScroll() { + if (iframe == null) { + return; + } + if (cancelScroll) { + cancelScroll = false; + if (isIOSMobile()) { + HTMLElement el = el(); + el.getStyle().setProperty("overflow", "auto"); + el.getStyle().setProperty("-webkit-overflow-scrolling", "touch"); + } else { + iframe.getStyle().setProperty("pointer-events", "auto"); + } + } + } + + @JSBody(params={"doc", "str"}, script="doc.write(str)") + private static native void documentWrite(HTMLDocument doc, String str); + + @JSBody(params={}, script="!!(\"srcdoc\" in document.createElement(\"iframe\"))") + private static native boolean supportsSrcdocAttribute(); + private boolean supportsSrcdocAttribute; + + @JSBody(params={"iframe"}, script="try{if(iframe.contentWindow.document){return false} else {return true}}catch(e){return true}") + private native static boolean isCORSRestricted(HTMLIFrameElement iframe); + + private boolean listenersInstalled; + private List frameListeners; + private void installFrameListeners() { + if (listenersInstalled) { + return; + } + listenersInstalled = true; + if (frameListeners == null) { + frameListeners = new ArrayList<>(); + frameListeners.add(new EventListener() { + + @Override + public void handleEvent(Event evt) { + //HTML5Implementation._log("In onLoad"); + // This hook is for javascript callbacks in the BrowserComponents. + // Most platforms use browser navigation callbacks to pass information + // from javascript to java. This involves attempting to change the page URL + // to a URL with an encoded message, the platform would parse the URL + // and then tell the browser to not navigate to it. + // We don't have that kind of control in iframes so instead, we provide + // this alternate hook that the BrowserComponent's callback will look for + // before it tries to pass the value via URL. + isCORSRestricted = isCORSRestricted(iframe); + + if (!isCORSRestricted) { + HTML5Implementation._log("Is NOT cors restricted"); + try { + for (String type : new String[]{"pointerdown", "pointerup", "pointermove", "mousedown", "mouseup", "mousemove", "touchstart", "touchend", "touchmove", isFirefox() ? "MozMousePixelScroll" : HTML5Implementation.getWheelEventType()}) { + // Comment on using MozMousePixelScroll in firefox (Nov. 2018) + // Through the rest of the app we use DOMMouseScroll for wheel events when in Firefox (see normalizeWheel.getEventType() defined in fontmetrics.js) + // However, consuming this event (i.e. evt.preventDefault() doesn't prevent scrolling + // in firefox. The MozMousePixelScroll event can be consumed, so, since the event router works by passing + // the events up to Codename one to be consumed (if there is a component over top of the iframe), + // we need to use this event in this case. + // The copyWheelEvent function (in fontmetrics) propagates this as a DOMMouseScroll event so that + // CN1 will deal with it as a regular wheel event. + // See https://stackoverflow.com/a/46612031/2935174 + + iframe.getContentWindow().addEventListener(type, eventRouter, true); + } + + } catch (Throwable t) { + HTML5Implementation._log("Failed to add event handlers to iframe, probably due to a CORS error"); + } + + installShouldLoadURLCallback(iframe, new ShouldLoadURLCallback() { + + @Override + public boolean shouldLoadURL(final JSObject url) { + new Thread() { + public void run() { + + parent.fireBrowserNavigationCallbacks(JS.unwrapString(url)); + } + }.start(); + + // We always return false since this will only be used for + // the javascript bridge and we don't want it to try to + // do a window.location load. + return false; + } + }); + } else { + HTML5Implementation._log("Is cors restricted"); + } + + new Thread() { + public void run() { + + //String url = useProxy ? proxyUrl : (iframe.getContentWindow()).getLocation().getFullURL(); + String url = useProxy ? proxyUrl : getIframeUrl(iframe); + parent.fireWebEvent("onStart", new ActionEvent(url)); + parent.fireWebEvent("onLoad", new ActionEvent(url)); + } + }.start(); + + } + + }); + /* + frameListeners.add(new EventListener() { + + @Override + public void handleEvent(Event evt) { + new Thread() { + public void run() { + + //String url = useProxy ? proxyUrl : (iframe.getContentWindow()).getLocation().getFullURL(); + String url = useProxy ? proxyUrl : getIframeUrl(iframe); + parent.fireWebEvent("onStart", new ActionEvent(url)); + parent.fireWebEvent("onLoad", new ActionEvent(url)); + } + }.start(); + + } + + }); + */ + } + if (iframe != null) { + for (EventListener e : frameListeners) { + iframe.addEventListener("load", e, false); + } + } + + + } + + private void uninstallFrameListeners() { + if (!listenersInstalled) { + return; + } + listenersInstalled = false; + if (frameListeners != null && iframe != null) { + for (EventListener e : frameListeners) { + iframe.removeEventListener("load", e, false); + } + } + } + + public HTML5BrowserComponent(HTMLElement el, Object p) { + super(wrapEl(el)); + supportsSrcdocAttribute = supportsSrcdocAttribute(); + id = ++nextId; + if (el == null) { + if (p instanceof BrowserComponent) { + parent = (BrowserComponent)p; + parent.fireWebEvent("onStart", new ActionEvent(CN.getProperty("browser.window.location.href", ""))); + parent.fireWebEvent("onLoad", new ActionEvent(CN.getProperty("browser.window.location.href", ""))); + installShouldLoadURLCallbackShared(new ShouldLoadURLCallback() { + + @Override + public boolean shouldLoadURL(final JSObject url) { + new Thread() { + public void run() { + + parent.fireBrowserNavigationCallbacks(JS.unwrapString(url)); + } + }.start(); + + // We always return false since this will only be used for + // the javascript bridge and we don't want it to try to + // do a window.location load. + return false; + } + }); + } + + return; + } + iframe=(HTMLIFrameElement)el; + el = (HTMLElement)(isIOSMobile() ? el.getParentNode() : el); + this.parent=(BrowserComponent)p; + if (isFirefox()) { + // For some unknown reason firefox won't deliver events to the iframe properly + // if it is a child of the container. We need to add it to the body itself. + el.getOwnerDocument().getBody().appendChild(el); + + } else { + HTML5Implementation.getInstance().peersContainer.appendChild(el); + } + isAdded = true; + el.getStyle().setProperty("display", "none"); + if (isIOSMobile()) { + // On iOS, iFrames won't scroll on their own. (Yes even in iOS 12). + // so we need to wrap it in a div, and make the div scrollable. + + + iframe.getStyle().setProperty("height", "100%"); + iframe.getStyle().setProperty("width", "100%"); + iframe.getStyle().setProperty("overflow", "auto"); + iframe.getStyle().setProperty("-webkit-overflow-scrolling", "touch"); + el.getStyle().setProperty("overflow", "auto"); + el.getStyle().setProperty("-webkit-overflow-scrolling", "touch"); + + } + Log.p("In HTML5BrowserComponent constructor.... installing frame listeners"); + installFrameListeners(); + + + + } + @JSBody(script="return /iPhone|iPod|iPad/.test(navigator.userAgent)") + private static native boolean isIOSMobile_(); + + private static boolean isIOSMobile() { + return false; + //return isIOSMobile_() && "true".equals(CN.getProperty("javascript.ios.iframe.scroll", "false")); + } + + @JSBody(script="return (typeof InstallTrigger !== 'undefined')") + private static native boolean isFirefox(); + + private static HTMLElement wrapEl(HTMLElement el) { + if (el == null) { + return HTML5Implementation.getInstance().window.getDocument().createElement("div"); + } + if (isIOSMobile()) { + HTMLElement wrapper = HTML5Implementation.getInstance().window.getDocument().createElement("div"); + if (el.getParentNode() != null) { + el.getParentNode().removeChild(el); + } + + wrapper.appendChild(el); + return wrapper; + } else { + return el; + } + } + + public void setURL(String url){ + if (iframe == null) { + return; + } + if (Boolean.TRUE.equals(parent.getClientProperty("javascript.useProxy")) ) { + useProxy = true; + } else { + useProxy = false; + } + if (useProxy) { + if (!proxyInitialized) { + proxyInitialized = true; + proxyHistory = new ArrayList(); + proxy = new HTML5Proxy(); + installLoadPageCallback(iframe, new LoadPageCallback() { + @Override + public void load(final String url) { + //System.out.println("Handling callback for url "+url); + new Thread() { + public void run() { + Display.getInstance().callSerially(new Runnable() { + public void run() { + setURL(url); + } + }); + } + }.start(); + + + + } + + }); + } + if (!doNotAddToHistory) { + if (proxyHistory.size() > proxyHistoryPos+1) { + List oldHistory = proxyHistory.subList(0, proxyHistoryPos+1); + proxyHistory = new ArrayList(); + proxyHistory.addAll(oldHistory); + } + proxyHistory.add(url); + proxyHistoryPos++; + } + proxy.load(url, new ActionListener() { + + @Override + public void actionPerformed(ActionEvent t) { + // NOt sure if we need to do anything here yet. + } + }); + } else { + iframe.setAttribute("src", url); + } + } + + @JSBody(params={"iframe"}, script="var src; try { src = iframe.contentWindow.location.href} catch (e) {src = iframe.src} return src") + private static native String getIframeUrl(HTMLIFrameElement iframe); + + public String getURL(){ + if (iframe == null) { + return Window.current().getLocation().getFullURL(); + } + return useProxy ? proxyUrl : getIframeUrl(iframe); + } + + public void reload(){ + setURL(getURL()); + } + + @JSBody(params={"el"}, script="return el.ownerDocument.contains(el)") + private native static boolean documentContains(HTMLElement el); + + + @Override + protected void initComponent() { + HTMLElement el = el(); + Log.p("In HTML5BrowserComponent::initComponent()"); + if (!documentContains(el)) { + Log.p("Not added to document yet. Appending..."); + uninstallFrameListeners(); + if (isFirefox()) { + // For some unknown reason firefox won't deliver events to the iframe properly + // if it is a child of the container. We need to add it to the body itself. + el.getOwnerDocument().getBody().appendChild(el); + + } else { + HTML5Implementation.getInstance().peersContainer.appendChild(el); + } + isAdded = true; + installFrameListeners(); + } else { + Log.p("Iframe was alreday added to document. not appending"); + } + el.getStyle().setProperty("display", "block"); + Log.p("Calling super initComponent"); + super.initComponent(); + + + } + + @Override + protected void deinitialize() { + super.deinitialize(); + isAdded = false; + } + + + + public void setPage(String content, String baseUrl){ + if (iframe == null) { + return; + } + if (!supportsSrcdocAttribute) { + DocumentExt doc = (DocumentExt)iframe.getContentWindow().getDocument(); + doc.open("text/htmlreplace"); + doc.write(content); + doc.close(); + + } else { + iframe.setAttribute("srcdoc", content); + } + } + + public void setProperty(String key, Object value){ + properties.put(key, value); + } + + public Object getProperty(String key){ + return properties.get(key); + } + + public void execute(String javascript){ + if (isCORSRestricted) { + throw new RuntimeException("Cannot execute javascript in this browser component because it is CORS-restricted. Javascript was "+javascript); + } + WindowExt win = iframe == null ? ((WindowExt)Window.current()) : (WindowExt)iframe.getContentWindow(); + + win.eval(javascript); + //Window win = iframe.getContentWindow(); + //win.getLocation().assign("javascript:"+javascript); + + } + + @JSBody(params={"win", "js"}, script="return ''+win.eval(js);") + private static native String evalStr(Window win, String js); + + public String executeAndReturnString(String javascript){ + if (isCORSRestricted) { + throw new RuntimeException("Cannot execute javascript in this browser component because it is CORS-restricted."); + } + //WindowExt win = (WindowExt)iframe.getContentWindow(); + Window win = iframe == null ? Window.current() : iframe.getContentWindow(); + return evalStr(win, javascript); + //return win.eval(javascript); + } + + public boolean hasBack() { + if (iframe == null) { + return false; + } + if (useProxy) { + return proxyHistoryPos > 0; + } else { + return true; + } + } + + public void back() { + if (iframe == null) { + return; + } + if (useProxy) { + if (proxyHistoryPos > 0) { + proxyHistoryPos--; + doNotAddToHistory = true; + setURL(proxyHistory.get(proxyHistoryPos)); + doNotAddToHistory = false; + } + } else { + if (isCORSRestricted) { + throw new RuntimeException("Cannot go back() in this browser component because it is CORS-restricted."); + } + iframe.getContentWindow().getHistory().back(); + } + } + + public boolean hasForward() { + if (iframe == null) { + return false; + } + // Well there's no easy way to know ... so let's just say yes. + if (useProxy) { + return proxyHistoryPos < proxyHistory.size()-1; + } + + return true; + } + + public void forward() { + if (iframe == null) { + return; + } + if (useProxy) { + if (proxyHistoryPos < proxyHistory.size()-1) { + proxyHistoryPos++; + doNotAddToHistory = true; + setURL(proxyHistory.get(proxyHistoryPos)); + doNotAddToHistory = false; + } + } else { + if (isCORSRestricted) { + throw new RuntimeException("Cannot go forward in this browser component because it is CORS-restricted."); + } + iframe.getContentWindow().getHistory().forward(); + } + } + + @JSBody(params={"url", "content", "iframe"}, script="return window.cn1.proxifyContent(url, content, iframe);") + private native static void proxifyContent(String url, String content, JSObject iframe); + + private class HTML5Proxy { + /* + private String getHost(String absUrl) { + int slashSlashPos = absUrl.indexOf("//"); + if (slashSlashPos < 0) { + return ""; + } + absUrl = absUrl.substring(slashSlashPos+2); + int slashPos = absUrl.indexOf("/"); + if (slashPos >= 0) { + absUrl = absUrl.substring(0, slashPos); + } + return absUrl; + } + + private String getHostName(String absUrl) { + absUrl = getHost(absUrl); + int colonPos = absUrl.indexOf(":"); + if (colonPos >= 0) { + absUrl = absUrl.substring(0, colonPos); + } + return absUrl; + } + + private String getHash(String absUrl) { + int hashPos = absUrl.indexOf("#"); + if (hashPos >= 0) { + absUrl = absUrl.substring(hashPos); + } + return absUrl; + } + + private String getProtocol(String absUrl) { + int colonPos = absUrl.indexOf(":"); + if (colonPos > 0) { + return absUrl.substring(0, colonPos+1); + } + return ""; + } + + private String getPort(String absUrl) { + absUrl = getHost(absUrl); + int colonPos = absUrl.indexOf(":"); + if (colonPos >= 0) { + return absUrl.substring(colonPos+1); + } else { + return ""; + } + } + + private String getPathname(String absUrl) { + absUrl = absUrl.substring(getHost(absUrl).length()); + int hashPos = absUrl.indexOf("#"); + if (hashPos>=0) { + absUrl = absUrl.substring(0, hashPos); + } + int qpos = absUrl.indexOf("?"); + if (qpos >= 0) { + absUrl = absUrl.substring(0, qpos); + } + return absUrl; + + } + + private String getSearch(String absUrl) { + int qpos = absUrl.indexOf("?"); + absUrl = absUrl.substring(qpos); + int hashPos = absUrl.indexOf("#"); + if (hashPos >= 0) { + absUrl = absUrl.substring(0, hashPos); + } + return absUrl; + } + + + private String getOrigin(String absUrl) { + int slashSlashPos = absUrl.indexOf("//"); + if (slashSlashPos >= 0) { + slashSlashPos = absUrl.indexOf("/", slashSlashPos+2); + } else { + slashSlashPos = absUrl.indexOf("/"); + } + + if (slashSlashPos >= 0) { + return absUrl.substring(0, slashSlashPos); + } else { + int qpos = absUrl.indexOf("?"); + if (qpos >= 0) { + return absUrl.substring(0, qpos); + } + int hashPos = absUrl.indexOf("#"); + if (hashPos >= 0) { + return absUrl.substring(0, hashPos); + } + return absUrl; + } + } + + private String getBaseUrl(String absUrl) { + int qpos = absUrl.indexOf("?"); + if (qpos >= 0) { + absUrl = absUrl.substring(0, qpos); + } + int hashPos = absUrl.indexOf("#"); + if (hashPos >= 0) { + absUrl = absUrl.substring(0, hashPos); + } + JSString jstr = JSString.valueOf(absUrl); + if (jstr.search(JSString.valueOf("\\.(html|htm|php|jsp|asp|xml|pl|py|cgi)$")) > 0 || jstr.search(JSString.valueOf("/$")) > 0) { + return absUrl.substring(absUrl.lastIndexOf("/")); + } else { + return absUrl; + } + } + + private String makeUrlAbsolute(String baseUrl, String url) { + if (baseUrl.charAt(baseUrl.length()-1) != '/') { + baseUrl += "/"; + } + if (url.length() > 1 && url.charAt(0) == '/' && url.charAt(1) == '/') { + return getProtocol(baseUrl) + url; + } + if (url.length() > 0 && url.charAt(0) == '/') { + return getOrigin(baseUrl) + url; + } + if ("".equals(getHost(url))) { + return baseUrl + url; + } + + return url; + } + */ + void load(final String url, ActionListener onComplete) { + final ConnectionRequest req = new ConnectionRequest(); + //System.out.println("Sending connection request to "+url); + req.setUrl(url); + req.addResponseListener(new ActionListener() { + + @Override + public void actionPerformed(NetworkEvent t) { + byte data[] = req.getResponseData(); + try { + proxyUrl = url; + //System.out.println("Setting proxyURL to "+url); + proxifyContent(url, new String(data, "UTF-8"), iframe); + //HTML5BrowserComponent.this.setPage(str, url); + + } catch (UnsupportedEncodingException ex) { + System.err.println("Failed to get content "+ex.getMessage()); + } + } + + }); + + NetworkManager.getInstance().addToQueue(req); + } + } + + + @JSFunctor + static interface LoadPageCallback extends JSObject { + void load(String url); + } + + @JSBody(params={"iframe","callback"}, script="jQuery(iframe).on('cn1load', function(evt, url){ callback(url);});") + native static void installLoadPageCallback(HTMLIFrameElement el, LoadPageCallback callback); + + + @JSFunctor + interface ShouldLoadURLCallback extends JSObject { + boolean shouldLoadURL(JSObject url); + } + + @JSBody(params={"iframe","callback"}, script="try {var win=iframe.contentWindow||iframe; win.cn1application = win.cn1application || {}; win.cn1application.shouldNavigate=callback;} catch (e) { console.log('Failed to install shouldNavigate in iframe for browser component.');}") + native static void installShouldLoadURLCallback(HTMLIFrameElement el, ShouldLoadURLCallback callback); + + @JSBody(params={"callback"}, script="try {var win=window; win.cn1application = win.cn1application || {}; win.cn1application.shouldNavigate=callback;} catch (e) { console.log('Failed to install shouldNavigate in iframe for browser component.');}") + native static void installShouldLoadURLCallbackShared(ShouldLoadURLCallback callback); + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserWindow.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserWindow.java new file mode 100644 index 0000000000..aebf02582a --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5BrowserWindow.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.io.Log; +import com.codename1.ui.BrowserWindow; +import com.codename1.ui.CN; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.util.EventDispatcher; +import java.util.Objects; +import java.util.Timer; +import org.teavm.jso.JSBody; +import org.teavm.jso.browser.TimerHandler; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; + + +/** + * A base class for JavaSE browser window implementations. + * @author shannah + * @since 7.0 + */ +public class HTML5BrowserWindow { + + private EventDispatcher loadListeners = new EventDispatcher(); + private EventDispatcher closeListeners = new EventDispatcher(); + private Window win; + private String url; + private String name; + private boolean closed; + private int intervalHandle; + public HTML5BrowserWindow(String url, String title) { + this.url=url; + name=title; + } + + + /** + * Adds listener to be notified on page load. + * @param l + */ + public void addLoadListener(ActionListener l) { + loadListeners.addListener(l); + } + + /** + * Removes page load listener. + * @param l + */ + public void removeLoadListener(ActionListener l) { + loadListeners.removeListener(l); + } + + + + /** + * Shows the window + */ + public void show() { + + win = Window.current().open("", ""); + win.addEventListener("close", new EventListener() { + @Override + public void handleEvent(Event evt) { + if (closed) { + return; + } + closed = true; + HTML5Implementation.getInstance().callSerially(new Runnable() { + public void run() { + fireCloseEvent(new ActionEvent(this)); + } + }); + } + }); + win.addEventListener("load", new EventListener() { + @Override + public void handleEvent(Event evt) { + + String newUrl; + try { + newUrl = win.getLocation().getFullURL(); + } catch (Throwable t) { + + newUrl = url; + } + final String fNewUrl = newUrl; + url = newUrl; + HTML5Implementation.getInstance().callSerially(new Runnable() { + public void run() { + + fireLoadEvent(new ActionEvent(fNewUrl)); + } + }); + + } + + }); + + win.setName(name); + win.getLocation().setFullURL(url); + intervalHandle = Window.setInterval(new TimerHandler() { + @Override + public void onTimer() { + if (win == null || isClosed(win)) { + Window.clearInterval(intervalHandle); + intervalHandle = 0; + + if (closed) { + return; + } + closed = true; + HTML5Implementation.callSerially(new Runnable() { + public void run() { + fireCloseEvent(new ActionEvent(null)); + } + + }); + + return; + } + try { + String newUrl = win.getLocation().getFullURL(); + if (!Objects.equals(newUrl, url)) { + url = newUrl; + final String fNewUrl = newUrl; + HTML5Implementation.callSerially(new Runnable() { + public void run() { + fireLoadEvent(new ActionEvent(fNewUrl)); + } + + }); + + + } + } catch (Throwable t) {} + } + }, 200); + + } + + @JSBody(params={"win"}, script="return win.closed") + private native static boolean isClosed(Window win); + + /** + * Sets the window size. + * @param width + * @param height + */ + public void setSize(final int width, final int height) { + if (win == null) { + return; + } + win.resizeTo(width, width); + } + + /** + * Sets the window title. + * @param title + */ + public void setTitle(final String title) { + this.name = title; + if (win != null) { + win.setName(title); + } + } + + /** + * Hides the window. + */ + public void hide() { + if (win == null) { + return; + } + win.close(); + } + + + /** + * Cleans up window resources. + */ + public void cleanup() { + hide(); + } + + + /** + * Evaluates Javascript + * @param req + */ + public void eval(BrowserWindow.EvalRequest req) { + throw new RuntimeException("eval() not supported yet"); + } + + + /** + * Adds listener to be notified when window is closed. + * @param l + */ + public void addCloseListener(ActionListener l) { + closeListeners.addListener(l); + } + + /** + * Removes window close listener. + * @param l + */ + public void removeCloseListener(ActionListener l) { + closeListeners.removeListener(l); + } + + /** + * Deliver event on close. + * @param evt + */ + protected void fireCloseEvent(ActionEvent evt) { + closeListeners.fireActionEvent(evt); + } + + /** + * Deliver event on load. The source of the event should be a string URL + * of the page that was loaded. + * @param evt + */ + protected void fireLoadEvent(ActionEvent evt) { + loadListeners.fireActionEvent(evt); + } +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Graphics.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Graphics.java new file mode 100644 index 0000000000..a7178c7803 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Graphics.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + + + + +import com.codename1.impl.html5.HTML5Implementation.NativeFont; +import com.codename1.impl.html5.HTML5Implementation.NativeImage; +import com.codename1.impl.html5.JSOImplementations.JSFontMetrics; +import com.codename1.impl.html5.graphics.ClearRect; +import com.codename1.impl.html5.graphics.ClipRect; +import com.codename1.impl.html5.graphics.ClipShape; +import com.codename1.impl.html5.graphics.ClipState; +import com.codename1.impl.html5.graphics.DrawArc; +import com.codename1.impl.html5.graphics.DrawImage; +import com.codename1.impl.html5.graphics.DrawLine; +import com.codename1.impl.html5.graphics.DrawPolygon; +import com.codename1.impl.html5.graphics.DrawRect; +import com.codename1.impl.html5.graphics.DrawRoundRect; +import com.codename1.impl.html5.graphics.DrawShape; +import com.codename1.impl.html5.graphics.DrawString; +import com.codename1.impl.html5.graphics.FillArc; +import com.codename1.impl.html5.graphics.FillLinearGradient; +import com.codename1.impl.html5.graphics.FillPolygon; +import com.codename1.impl.html5.graphics.FillRadialGradient; +import com.codename1.impl.html5.graphics.FillRect; +import com.codename1.impl.html5.graphics.FillRoundRect; +import com.codename1.impl.html5.graphics.FillShape; +import com.codename1.impl.html5.graphics.SetTransform; +import com.codename1.impl.html5.graphics.TileImage; +import com.codename1.impl.html5.graphics.ExecutableOp; +import com.codename1.teavm.geom.JSAffineTransform; +import com.codename1.ui.Stroke; +import com.codename1.ui.Transform; +import com.codename1.ui.geom.GeneralPath; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.geom.Shape; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.canvas.CanvasRenderingContext2D; +import org.teavm.jso.canvas.ImageData; +import org.teavm.jso.dom.html.HTMLCanvasElement; + +/** + * ####################################################################### + * ####################################################################### + * + * Bundle one canvas and two paints to get one graphics object. + */ +public class HTML5Graphics { + + private final JavaScriptRenderState renderState = new JavaScriptRenderState(); + private HTMLCanvasElement canvas; + private CanvasRenderingContext2D context; + //private Paint paint; + HTML5Implementation impl; + private boolean inClip = false; + private Rectangle clipBounds=new Rectangle(); + private boolean clipBoundsDirty=true; + private GeneralPath clipShape = new GeneralPath(); + + private boolean isClipShape; + private Transform transform, clipTransform; + private boolean transformApplied = false; + + + private final Rectangle clipRect = new Rectangle(); + private final JavaScriptPrimitiveRenderAdapter primitiveRenderAdapter = + new JavaScriptPrimitiveRenderAdapter(renderState, + new JavaScriptPrimitiveRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + operation.execute(context); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + private final JavaScriptImageTransformRenderAdapter imageTransformRenderAdapter = + new JavaScriptImageTransformRenderAdapter(renderState, + new JavaScriptImageTransformRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + operation.execute(context); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + private final JavaScriptShapeGradientRenderAdapter shapeGradientRenderAdapter = + new JavaScriptShapeGradientRenderAdapter(renderState, + new JavaScriptShapeGradientRenderAdapter.OperationSink() { + @Override + public void submit(ExecutableOp operation) { + operation.execute(context); + } + }, JavaScriptExecutableOpFactory.INSTANCE); + + //private final Path tmppath = new Path(); + //private final static PorterDuffXfermode PORTER = new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER); + + HTML5Graphics(HTML5Implementation impl, HTMLCanvasElement canvas) { + this.canvas = canvas; + this.context = (CanvasRenderingContext2D)canvas.getContext("2d"); + + this.impl = impl; + this.clipRect.setWidth(canvas.getWidth()); + this.clipRect.setHeight(canvas.getHeight()); + //transform = JSAffineTransform.Factory.getTranslateInstance(0, 0); + //paint.setAntiAlias(true); + + if(context != null) { + context.save(); + } + //transform = Transform.makeIdentity(); + } + + + public ClipState getClipState() { + return renderState.getClipState(); + } + + protected final JavaScriptRenderState getRenderState() { + return renderState; + } + + public HTMLCanvasElement getCanvas(){ + return canvas; + } + + void setCanvas(HTMLCanvasElement canvas) { + this.canvas = canvas; + this.context = null; + if(canvas != null) { + this.context = (CanvasRenderingContext2D)canvas.getContext("2d"); + context.save(); + } + } + + void setCanvasNoSave(HTMLCanvasElement canvas) { + this.canvas = canvas; + this.context = null; + if(canvas != null) { + this.context = (CanvasRenderingContext2D)canvas.getContext("2d"); + + } + } + + NativeFont getFont() { + return renderState.getFont(); + } + + void setFont(NativeFont font) { + renderState.setFont(font); + context.setFont(font.getCSS()); + + + } + + public static String color(int rgb){ + int red = (rgb >> 16) & 0xFF; + int green = (rgb >> 8) & 0xFF; + int blue = rgb & 0xFF; + //int alpha = (rgb >> 24) & 0xFF; + return "rgb("+red+","+green+","+blue+")"; + } + + public static String colorWithAlpha(int argb) { + int red = (argb >> 16) & 0xFF; + int green = (argb >> 8) & 0xFF; + int blue = argb & 0xFF; + int alpha = (argb >> 24) & 0xFF; + return "rgba("+red+","+green+","+blue+","+(alpha/255f)+")"; + } + + void setColor(int color){ + //System.out.println("Setting color "+color(color)); + renderState.setColor(color); + this.context.setFillStyle(color(color)); + this.context.setStrokeStyle(color(color)); + + } + + void setColorWithAlpha(int color) { + //System.out.println("Setting color "+color(color)); + renderState.setColor(color); + this.context.setFillStyle(colorWithAlpha(color)); + this.context.setStrokeStyle(colorWithAlpha(color)); + } + + void setAlpha(int alpha) { + renderState.setAlpha(alpha); + this.context.setGlobalAlpha(alpha / 255.0); + } + + int getAlpha() { + return renderState.getAlpha(); + //return (int)(context.getGlobalAlpha() * 255); + } + + CanvasRenderingContext2D getContext() { + return context; + } + + + public void drawImage(Object img, int x, int y) { + imageTransformRenderAdapter.drawImage((NativeImage)img, x, y); + + } + + + public void tileImage(Object img, int x, int y, int w, int h) { + imageTransformRenderAdapter.tileImage((NativeImage)img, x, y, w, h); + } + + + + + public void setTransform(Transform t) { + setTransform(t, true); + + } + + public void applyTransform() { + if (!transformApplied) { + if (transform == null) { + transform = Transform.makeIdentity(); + } + imageTransformRenderAdapter.applyTransform((JSAffineTransform)transform.getNativeTransform(), true); + transformApplied = true; + } + } + + public void setTransformChanged() { + transformApplied = false; + clipBoundsDirty = true; + } + + public void setTransform(Transform t, boolean replace) { + if (transform == null) { + transform = Transform.makeIdentity(); + } + if (replace) { + transform = t; + } else { + transform.concatenate(t); + } + setTransformChanged(); + applyTransform(); + } + + public boolean isTransformSupported() { + return true; + } + + public void rotate(double angle) { + if (transform != null) { + transform.rotate((float)angle, 0, 0); + setTransformChanged(); + applyTransform(); + } else { + setTransform(Transform.makeRotation((float)angle, 0, 0), false); + } + } + + public void rotate(double angle, int pivotX, int pivotY) { + if (transform != null) { + transform.rotate((float)angle, pivotX, pivotY); + setTransformChanged(); + applyTransform(); + } else { + setTransform(Transform.makeRotation((float)angle, pivotX, pivotY), false); + } + } + +// public void shear(double shx, double shy) { +// +// transform = transform != null ? transform.shear(shx, shy) : +// JSAffineTransform.Factory.getShearInstance(shx, shy); +// setTransform(transform, false); +// transformDirty = true; +// } + + public Transform getTransform() { + if (transform == null) { + transform = Transform.makeIdentity(); + } + return transform; + } + + + + public void scale(double sx, double sy) { + if (transform != null) { + transform.scale((float)sx, (float)sy); + setTransformChanged(); + applyTransform(); + } else { + setTransform(Transform.makeScale((float)sx, (float)sy)); + } + } + + public void drawImage(Object img, int x, int y, int w, int h) { + imageTransformRenderAdapter.drawImage((NativeImage)img, x, y, w, h); + } + + + public void drawLine(int x1, int y1, int x2, int y2) { + primitiveRenderAdapter.drawLine(x1, y1, x2, y2); + } + + + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + new DrawPolygon(xPoints, yPoints, nPoints, getColor(), getAlpha()).execute(context); + } + + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + new FillPolygon(xPoints, yPoints, nPoints, getColor(), getAlpha()).execute(context); + } + + public void drawRGB(int[] rgbData, int offset, int x, + int y, int w, int h, boolean processAlpha) { + if (offset != 0){ + int[] copy = new int[w*h]; + System.arraycopy(rgbData, offset, copy, 0, w*h); + rgbData = copy; + } + NativeImage img = (NativeImage)impl.createImage(rgbData, w, h); + drawImage(img, x, y, w, h); + } + + public void drawRect(int x, int y, int width, int height) { + primitiveRenderAdapter.drawRect(x, y, width, height); + } + + public void drawRoundRect(int x, int y, int width, + int height, int arcWidthInt, int arcHeightInt) { + new DrawRoundRect(x, y, width, height, arcWidthInt, arcHeightInt, getColor(), getAlpha()).execute(context); + } + + public void drawString(String str, int x, int y) { + primitiveRenderAdapter.drawString(str, x, y); + } + + public void drawArc(int x, int y, int width, int height, + int startAngle, int arcAngle) { + new DrawArc(x, y, width, height, startAngle, arcAngle, getColor(), getAlpha()).execute(context); + } + + public void drawShape(Shape shape, Stroke stroke) { + shapeGradientRenderAdapter.drawShape(shape, stroke); + } + + public void fillShape(Shape shape) { + shapeGradientRenderAdapter.fillShape(shape); + } + + @JSBody(params={"o"}, script="console.log(o)") + private static native void log(JSObject o); + + + public void fillArc(int x, int y, int width, int height, + int startAngle, int arcAngle) { + new FillArc(x, y, width, height, startAngle, arcAngle, getColor(), getAlpha()).execute(context); + } + + public void fillRect(int x, int y, int width, int height) { + primitiveRenderAdapter.fillRect(x, y, width, height); + } + + public void clearRect(int x, int y, int width, int height) { + primitiveRenderAdapter.clearRect(x, y, width, height); + } + + public void fillRoundRect(int x, int y, int width, + int height, int arcWidthInt, int arcHeightInt) { + new FillRoundRect(x, y, width, height, arcWidthInt, arcHeightInt, getColor(), getAlpha()).execute(context); + + } + + + + + + private void calculateClipBounds() { + if (clipBoundsDirty) { + clipBoundsDirty = false; + Rectangle projectedShape = getCurrentClipProjection().getBounds(); + clipBounds.setBounds(projectedShape.getX(), projectedShape.getY(), projectedShape.getWidth(), projectedShape.getHeight()); + } + } + + + public int getClipHeight() { + calculateClipBounds(); + return clipBounds.getHeight(); + } + + + public int getClipWidth() { + calculateClipBounds(); + return clipBounds.getWidth(); + } + + + public int getClipX() { + calculateClipBounds(); + return clipBounds.getX(); + } + + + public int getClipY() { + calculateClipBounds(); + return clipBounds.getY(); + } + + private Transform getInverseTransform() { + if (transform == null) return null; + return transform.getInverse(); + } + + private Shape getCurrentClipProjection() { + if (isClipShape) { + GeneralPath out = new GeneralPath(clipShape); + Transform t = Transform.makeIdentity(); + if (clipTransform != null && !clipTransform.isIdentity()) { + t.concatenate(clipTransform); + } + if (transform != null && !transform.isIdentity()) { + t.concatenate(transform.getInverse()); + } + if (!t.isIdentity()) { + out.transform(t); + } + return out; + } else { + if (transform != null && !transform.isIdentity()) { + GeneralPath out = new GeneralPath(); + out.setRect(clipRect, transform.getInverse()); + return out; + } else { + return clipRect; + } + } + } + + + public void setClip(Shape shape) { + clipShape.reset(); + clipShape.setShape(shape, null); + isClipShape = true; + clipTransform = transform == null ? null : transform.copy(); + JSAffineTransform t = null; + if (transform != null) { + t = (JSAffineTransform)transform.getNativeTransform(); + } + clipBoundsDirty = true; + imageTransformRenderAdapter.setClipShape(shape, t); + //upcoming.add(new ClipShape(shape, t)); + } + + private void clipShape(Shape shape) { + if (!isClipShape) { + isClipShape = true; + clipShape.reset(); + clipShape.setShape(clipRect, null); + clipTransform = null; + } + GeneralPath p = (GeneralPath)getCurrentClipProjection(); + p.intersect(shape); + setClip(p); + } + + public void setClip(int x, int y, int width, int height) { + if (transform != null && !transform.isIdentity()) { + setClip(new Rectangle(x, y, width, height)); + return; + } + isClipShape = false; + clipRect.setBounds(x, y, width, height); + clipBoundsDirty = true; + primitiveRenderAdapter.setClipRect(x, y, width, height); + + } + + + + public void clipRect(int x, int y, int width, int height) { + Rectangle rect = new Rectangle(x, y, width, height); + if (isClipShape || transform != null && !transform.isIdentity()) { + clipShape(rect); + return; + } + + if (rect.contains(clipRect)) { + return; + } + clipRect.intersection(rect, clipRect); + clipBoundsDirty = true; + primitiveRenderAdapter.setClipRect(clipRect.getX(), clipRect.getY(), clipRect.getWidth(), clipRect.getHeight()); + } + + public int getColor() { + return renderState.getColor(); + } + + public void resetAffine() { + if (transform == null) { + transform = Transform.makeIdentity(); + } else { + transform.setIdentity(); + } + setTransformChanged(); + applyTransform(); + } + + + + + public int charsWidth(Object nativeFont, char[] ch, int offset, int length) { + return JavaScriptTextMetricsAdapter.charsWidth(new JavaScriptTextMetricsAdapter.FontMetricsContext() { + @Override + public String getCurrentFont() { + return context.getFont(); + } + + @Override + public void setCurrentFont(String fontCss) { + context.setFont(fontCss); + } + + @Override + public int measureWidth(String text) { + return context.measureText(text).getWidth(); + } + }, new JavaScriptTextMetricsAdapter.FontCssSupplier() { + @Override + public String getCss(NativeFont font) { + return font.getCSS(); + } + + @Override + public int getHeight(NativeFont font) { + return font.fontHeight(); + } + + @Override + public int getAscent(NativeFont font) { + return font.fontAscent(); + } + }, (NativeFont) nativeFont, ch, offset, length); + } + + public int stringWidth(Object nativeFont, String str) { + return JavaScriptTextMetricsAdapter.stringWidth(new JavaScriptTextMetricsAdapter.FontMetricsContext() { + @Override + public String getCurrentFont() { + return context.getFont(); + } + + @Override + public void setCurrentFont(String fontCss) { + context.setFont(fontCss); + } + + @Override + public int measureWidth(String text) { + return context.measureText(text).getWidth(); + } + }, new JavaScriptTextMetricsAdapter.FontCssSupplier() { + @Override + public String getCss(NativeFont font) { + return font.getCSS(); + } + + @Override + public int getHeight(NativeFont font) { + return font.fontHeight(); + } + + @Override + public int getAscent(NativeFont font) { + return font.fontAscent(); + } + }, (NativeFont) nativeFont, str); + } + + + + int getFontHeight(Object nativeFont){ + return JavaScriptTextMetricsAdapter.getFontHeight(new JavaScriptTextMetricsAdapter.FontCssSupplier() { + @Override + public String getCss(NativeFont font) { + return font.getCSS(); + } + + @Override + public int getHeight(NativeFont font) { + return font.fontHeight(); + } + + @Override + public int getAscent(NativeFont font) { + return font.fontAscent(); + } + }, (NativeFont) nativeFont); + } + + int getFontAscent(Object nativeFont){ + return ((NativeFont)nativeFont).fontAscent(); + } + + int getFontDescent(Object nativeFont){ + return JavaScriptTextMetricsAdapter.getFontDescent(new JavaScriptTextMetricsAdapter.FontCssSupplier() { + @Override + public String getCss(NativeFont font) { + return font.getCSS(); + } + + @Override + public int getHeight(NativeFont font) { + return font.fontHeight(); + } + + @Override + public int getAscent(NativeFont font) { + return font.fontAscent(); + } + }, (NativeFont) nativeFont); + } + +// int getFontLeading(Object nativeFont){ +// String oldFont = context.getFont(); +// context.setFont(((NativeFont)nativeFont).getCSS()); +// //this.canvas.getStyle().setProperty("font", nativeFont+""); +// int out = (int)Math.round(((JSFontMetrics)context.measureText(alphabet)).getLeading()); +// context.setFont(oldFont); +// return out; +// +// +// } + + void clear(){ + context.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + } + + public void fillLinearGradient(int x, int y, int width, int height, int startColor, int endColor, boolean horizontal) { + shapeGradientRenderAdapter.fillLinearGradient(x, y, width, height, startColor, endColor, horizontal); + } + + public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height, int startAngle, int arcAngle) { + shapeGradientRenderAdapter.fillRadialGradient(x, y, width, height, startColor, endColor, startAngle, arcAngle); + } + + public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height) { + fillRadialGradient(startColor, endColor, x, y, width, height, 0, 360); + } + + + + + + + + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java new file mode 100644 index 0000000000..f8da6b12b3 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -0,0 +1,9148 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; +import com.codename1.capture.VideoCaptureConstraints; +import com.codename1.compat.java.util.Objects; +import com.codename1.components.InteractionDialog; +import com.codename1.components.SpanLabel; +import com.codename1.components.ToastBar; +import com.codename1.db.Database; +import com.codename1.teavm.io.ArrayBufferInputStream; +import com.codename1.impl.CodenameOneImplementation; +import com.codename1.impl.CodenameOneThread; +import com.codename1.impl.html5.JSOImplementations.AnimationFrameCallback; +import com.codename1.impl.html5.JSOImplementations.CN1Native; +import com.codename1.impl.html5.JSOImplementations.HTMLIFrameElement; +import com.codename1.impl.html5.JSOImplementations.HTMLMediaElement; +import com.codename1.impl.html5.JSOImplementations.ImageExt; +import com.codename1.impl.html5.JSOImplementations.KeyEvent; +import com.codename1.impl.html5.JSOImplementations.Navigator; +import com.codename1.impl.html5.JSOImplementations.TextElement; +import com.codename1.impl.html5.JSOImplementations.WheelEvent; +import com.codename1.impl.html5.JSOImplementations.WindowExt; +import com.codename1.impl.html5.JSOImplementations.WindowLocation; +import com.codename1.impl.html5.components.ContextMenu; +import com.codename1.impl.html5.database.DatabaseImpl; +import com.codename1.impl.html5.graphics.ClipRect; +import com.codename1.impl.html5.graphics.ExecutableOp; +import com.codename1.impl.html5.videojs.JSVideoCaptureConstraintsCompiler; +import com.codename1.impl.html5.videojs.VideoJS; + + +import com.codename1.io.FileSystemStorage; +import com.codename1.io.Log; +import com.codename1.io.Util; +import com.codename1.l10n.L10NManager; +import com.codename1.location.LocationManager; +import com.codename1.media.Media; +import com.codename1.media.MediaRecorderBuilder; +import com.codename1.messaging.Message; +import com.codename1.push.PushCallback; +import com.codename1.social.GoogleImpl; +import com.codename1.teavm.ext.localforage.LocalForage; +import com.codename1.teavm.ext.localforage.LocalForage.ItemSavedListener; +import com.codename1.teavm.ext.usermedia.PhotoCapture; +import com.codename1.teavm.ext.websql.WebSQL; +import com.codename1.teavm.geom.JSAffineTransform; +import com.codename1.teavm.io.BlobUtil; +import com.codename1.teavm.jso.io.Blob; +import com.codename1.teavm.jso.io.FileList; +import com.codename1.teavm.jso.util.EventUtil; +import com.codename1.teavm.jso.util.JSDateFormat; +import com.codename1.teavm.jso.util.JSNumberFormat; +import com.codename1.ui.Accessor; +import com.codename1.ui.BrowserComponent; +import com.codename1.ui.BrowserWindow; +import com.codename1.ui.Button; +import com.codename1.ui.CN; +import static com.codename1.ui.CN.invokeAndBlock; +import com.codename1.ui.Component; +import com.codename1.ui.ComponentSelector; +import com.codename1.ui.Display; +import com.codename1.ui.Font; +import com.codename1.ui.FontImage; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; +import com.codename1.ui.Label; +import com.codename1.ui.PeerComponent; +import com.codename1.ui.Sheet; +import com.codename1.ui.Stroke; +import com.codename1.ui.TextArea; +import com.codename1.ui.TextField; +import com.codename1.ui.TextSelection; +import com.codename1.ui.Transform; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.ActionSource; +import com.codename1.ui.events.DataChangedListener; +import com.codename1.ui.events.FocusListener; +import com.codename1.ui.events.MessageEvent; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.geom.Shape; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; +import com.codename1.ui.layouts.GridLayout; +import com.codename1.ui.plaf.Style; +import com.codename1.ui.plaf.UIManager; +import com.codename1.ui.util.ImageIO; +import com.codename1.ui.util.Resources; +import com.codename1.ui.util.UITimer; +import com.codename1.util.AsyncResource; +import com.codename1.util.EasyThread; +import com.codename1.util.FailureCallback; +import com.codename1.util.StringUtil; +import com.codename1.util.SuccessCallback; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.util.ArrayList; +import java.util.Date; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import org.teavm.classlib.impl.tz.DateTimeZone; +import org.teavm.classlib.impl.tz.DateTimeZoneProvider; +import org.teavm.interop.SuppressSyncErrors; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.ajax.ReadyStateChangeHandler; +import org.teavm.jso.ajax.XMLHttpRequest; +import org.teavm.jso.browser.TimerHandler; +import org.teavm.jso.browser.Window; +import org.teavm.jso.canvas.CanvasPattern; +import org.teavm.jso.canvas.CanvasRenderingContext2D; +import org.teavm.jso.canvas.ImageData; +import org.teavm.jso.core.JSArray; +import org.teavm.jso.core.JSFunction; +import org.teavm.jso.core.JSNumber; +import org.teavm.jso.core.JSString; +import org.teavm.jso.dom.css.CSSStyleDeclaration; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.events.MouseEvent; +import org.teavm.jso.dom.html.HTMLButtonElement; +import org.teavm.jso.dom.html.HTMLCanvasElement; +import org.teavm.jso.dom.html.HTMLDocument; +import org.teavm.jso.dom.html.HTMLElement; +import org.teavm.jso.dom.html.HTMLImageElement; +import org.teavm.jso.dom.html.HTMLInputElement; +import org.teavm.jso.dom.html.HTMLTextAreaElement; +import org.teavm.jso.typedarrays.ArrayBuffer; +import org.teavm.jso.typedarrays.Float64Array; +import org.teavm.jso.typedarrays.Uint8Array; +import org.teavm.jso.typedarrays.Uint8ClampedArray; + +/** + * + * @author shannah + */ +public class HTML5Implementation extends CodenameOneImplementation { + + private L10NManager l10n; + private int density; + private static final String STORAGE_KEY_PREFIX = JavaScriptRuntimeFacade.STORAGE_KEY_PREFIX; + private static final String FILE_SYSTEM_PREFIX = JavaScriptRuntimeFacade.FILE_SYSTEM_PREFIX; + private static HTML5Implementation instance; + private boolean shiftKeyDown; + private BufferedGraphics graphics; + Window window; + private HTMLCanvasElement canvas; + private HTMLCanvasElement scratchBuffer; + HTMLCanvasElement outputCanvas; + private final JavaScriptRenderingBackend renderingBackend = new BrowserDomRenderingBackend(); + private EventListener onMouseDown, onMouseUp, onTouchStart, onTouchEnd, onMouseMove, onTouchMove, hitTest, onPaste; + + // This event listener can be assigned to listen to native mouse events + // and handle them directly. + private EventListener nativeEventListener; + + private JSFunction onMouseMoveHandle, onTouchMoveHandle, onPointerMoveHandle; + private NativeFont defaultFont; + private String pendingTextChanges; + private TextArea currentEditingField; + private HTMLInputElement currentInputField; + private boolean editingStartingUp; + private static double devicePixelRatio=-1; + private EasyThread nativeEdt; + + // Used for key press/release. We record the last char code + // in keypressed because the charcode isn't passed to keydown and keyup + // listeners -- only keypressed. + private int lastCharCode; + + private List mouseUpListeners = new ArrayList(); + + private int defaultFileSystemSize=104857600; // 100 Megs + + final Object editingLock=new Object(); + + private Form _getCurrent() { + return getCurrentForm(); + } + + + private AnimationFrameCallback animationFrameCallback; + + private JavaScriptRenderQueueState pendingDisplay=new JavaScriptRenderQueueState(); + + + + + /** + * Used to transform URLs that are to be fetched using a network connection + * to use a proxy. + */ + private URLProxifier urlProxifier; + + + private String photosPath="/photos"; + + private class BrowserDomRenderingBackend implements JavaScriptRenderingBackend { + @Override + public HTMLCanvasElement createCanvas(int width, int height) { + HTMLCanvasElement canvas = (HTMLCanvasElement)window.getDocument().createElement("canvas"); + canvas.setWidth(width); + canvas.setHeight(height); + return canvas; + } + + @Override + public HTMLImageElement createImageElement() { + return (HTMLImageElement)window.getDocument().createElement("img"); + } + + @Override + public HTMLImageElement createCrossOriginImageElement(String sourceUrl) { + HTMLImageElement image = createImageElement(); + image.setAttribute("crossorigin", "anonymous"); + image.setSrc(sourceUrl); + return image; + } + + @Override + public HTMLImageElement createBlobImageElement(Blob blob) { + return createCrossOriginImageElement(BlobUtil.createObjectURL(blob)); + } + + @Override + public HTML5Graphics createGraphics(HTML5Implementation implementation, HTMLCanvasElement canvas) { + return new HTML5Graphics(implementation, canvas); + } + + @Override + public CanvasRenderingContext2D getContext(HTMLCanvasElement canvas) { + return (CanvasRenderingContext2D)canvas.getContext("2d"); + } + + @Override + public void drawLoadedImage(CanvasRenderingContext2D context, HTMLImageElement image, int x, int y, int width, int height) { + context.drawImage(image, x, y, width, height); + } + + @Override + public void drawMutableSurface(CanvasRenderingContext2D context, HTMLCanvasElement canvas, int x, int y, int width, int height) { + context.drawImage(canvas, x, y, width, height); + } + + @Override + public CanvasPattern createLoadedImagePattern(CanvasRenderingContext2D context, HTMLImageElement image) { + return context.createPattern(image, "repeat"); + } + + @Override + public CanvasPattern createMutableSurfacePattern(CanvasRenderingContext2D context, HTMLCanvasElement canvas) { + return context.createPattern(canvas, "repeat"); + } + + @Override + public ImageData readLoadedImageData(HTMLImageElement image, int x, int y, int width, int height) { + HTMLCanvasElement canvas = createCanvas(width, height); + CanvasRenderingContext2D context = getContext(canvas); + context.drawImage(image, x, y, width, height, 0, 0, width, height); + return context.getImageData(0, 0, width, height); + } + + @Override + public ImageData readMutableSurfaceData(HTMLCanvasElement canvas, int x, int y, int width, int height) { + return getContext(canvas).getImageData(x, y, width, height); + } + + @Override + public void writeImageData(HTMLCanvasElement canvas, ImageData imageData, int width, int height) { + CanvasRenderingContext2D context = getContext(canvas); + context.clearRect(0, 0, width, height); + context.putImageData(imageData, 0, 0, 0, 0, width, height); + } + + @Override + public void scaleLoadedImageToCanvas(HTMLCanvasElement canvas, HTMLImageElement image, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight) { + getContext(canvas).drawImage(image, 0, 0, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight); + } + + @Override + public void scaleMutableSurfaceToCanvas(HTMLCanvasElement canvas, HTMLCanvasElement sourceCanvas, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight) { + getContext(canvas).drawImage(sourceCanvas, 0, 0, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight); + } + + @Override + public Blob toImageBlob(HTMLCanvasElement canvas, String mimeType, float quality) { + return BlobUtil.canvasToBlob(canvas, mimeType, quality); + } + + @Override + public void repaintCurrentForm() { + Form current = Display.getInstance().getCurrent(); + if (current != null) { + current.repaint(); + } + } + } + + /** + * We don't have an API yet to auto-detect the device's camera dimensions + * so we'll specify some hard defaults here and possibly abstract it further later. + * This article is useful on this topic: + * https://webrtchacks.com/how-to-figure-out-webrtc-camera-resolutions/ + */ + private int cameraWidth=960; + + private int cameraHeight=720; + + @Override + public boolean isShiftKeyDown() { + return shiftKeyDown; + } + + MouseEvent lastMouseEvent; + + @Override + public boolean isRightMouseButtonDown() { + if (lastMouseEvent !=null) { + return lastMouseEvent.getButton() == 2; + } + return false; + } + + + + /** + * @return the urlProxifier + */ + public URLProxifier getUrlProxifier() { + return urlProxifier; + } + + /** + * @param urlProxifier the urlProxifier to set + */ + public void setUrlProxifier(URLProxifier urlProxifier) { + this.urlProxifier = urlProxifier; + } + + private final JavaScriptPointerSessionState pointerState = new JavaScriptPointerSessionState(); + + public int getLastTouchUpX() { + return pointerState.getLastTouchUpX(); + } + + public int getLastTouchUpY() { + return pointerState.getLastTouchUpY(); + } + + private HashSet keysDown = new HashSet(); + + @JSBody(params={}, script="window.onbeforeunload=function(){return 'Leaving or refreshing the page may cause you to lose unsaved data.';}") + private native static void installBeforeUnload(); + + @JSBody(params={}, script="return window.onbeforeunload") + private native static JSObject getBeforeUnloadHandler(); + + @JSBody(params={"handler"}, script="window.onbeforeunload=handler") + private native static void setBeforeUnloadHandler(JSObject handler); + + private int getClientX(MouseEvent evt) { + int x = evt.getClientX(); + if (x == -1) { + return x; + } + + return (int)(x * getDevicePixelRatio()); + } + + private int getClientY(MouseEvent evt) { + int y = evt.getClientY(); + if (y == -1) { + return y; + } + return (int)((y + getScrollY_()) * getDevicePixelRatio()); + } + + private boolean hitTest(int x, int y) { + if (outputCanvas != null) { + CanvasRenderingContext2D ctx = (CanvasRenderingContext2D)outputCanvas.getContext("2d"); + if (ctx != null) { + try { + ImageData p = ctx.getImageData(x, y, 1, 1); + int pixelLen = p.getData().getLength(); + if (pixelLen == 4) { + if (p.getData().get(3) == 0) { + return false; + } + } + } catch (Exception ex){} + } + } + + return true; + + } + + + @JSBody(params={"event"}, script="cn1CopyEventToNativePeers(event);") + private native static void copyEventsToNativePeers(Event event); + + @JSBody(params={"str"}, script="if (window.cn1Debug) console.log(str);") + native static void _debug(String str); + + @JSBody(params={"obj"}, script="if (window.cn1Debug) console.log(obj);") + native static void _debugObj(JSObject obj); + @JSBody(params={"obj"}, script="console.log(obj);") + public native static void _logObj(JSObject obj); + + /** + * Container for all peer components. + */ + HTMLElement peersContainer; + + + /** + * Meant to be like the Runnable interface, but as a pure JS + * function + */ + @JSFunctor + public static interface JSRunnable extends JSObject { + public void run(); + } + + /** + * Callbacks that are run 300ms after a pointer press event from inside the pointer + * handler to perform things that require user interaction. These are necessary for things + * like movies that *cannot* be played programmatically because they require user interaction + * on platforms like Android and iOS. + * + */ + private JSArray backSideHooks = JSArray.create(); + public void addBacksideHook(JSRunnable r) { + backSideHooks.push(r); + } + + // Count the number of backside hook calls that are queued up + private int backsideHooksSemaphore = 0; + + /** + * Checks to see if there is a pending callback for a backside hook. + * I.e. If you add a backside hook to be executed RIGHT now, will be be executed + * in this batch, or will it need to wait for another user interaction. + * @return + */ + public boolean isBacksideHookAvailable() { + return backsideHooksIntervalHandle != 0 || backsideHooksSemaphore > 0; + } + + /** + * Backside hooks are just a tricky way to execute actions in response to user actions. + * Some things, like playing media, can only happen in response to a user interaction + * on mobile devices. Unfortunately, CN1 uses its own event thread so none of the events + * technically happen in response to a user event - so far as the browser is concerned. + * So we add some hooks that are run in a setTimeout() inside the actual native event handlers + * where we can queue actions that be be performed. + * @param timeout + */ + private void runBacksideHooksInTimeout(int timeout) { + backsideHooksSemaphore++; + //_log("Incrementing backsideHooksSemaphore: "+backsideHooksSemaphore); + Window.setTimeout(new TimerHandler() { + @Override + public void onTimer() { + backsideHooksSemaphore--; + //_log("Decrementing backsideHooksSemaphore: "+backsideHooksSemaphore); + runBacksideHooks(); + } + }, timeout); + } + + @JSBody(params={}, script="while (window.cn1NativeBacksideHooks.length > 0) {" + + " var f = window.cn1NativeBacksideHooks.shift();" + + " try {f();} catch (e){console.log(e);}" + + "}") + private native static void runPendingNativeBacksideHooks(); + + private int backsideHooksIntervalTimeout; + private int backsideHooksIntervalHandle; + + private void startBacksideHooksInterval() { + if (backsideHooksIntervalTimeout > 0) { + startBacksideHooksInterval(backsideHooksIntervalTimeout); + } + } + + private void startBacksideHooksInterval(int interval) { + if (backsideHooksIntervalHandle != 0) { + Window.clearInterval(backsideHooksIntervalHandle); + backsideHooksIntervalHandle = 0; + } + backsideHooksIntervalHandle = Window.setInterval(new TimerHandler() { + @Override + public void onTimer() { + runBacksideHooks(); + } + + }, interval); + } + + private void stopBacksideHooksInterval() { + if (backsideHooksIntervalHandle != 0) { + Window.clearInterval(backsideHooksIntervalHandle); + backsideHooksIntervalHandle = 0; + } + } + + /** + * Runs all of the pending backside hooks. + */ + public void runBacksideHooks() { + runPendingNativeBacksideHooks(); + while (backSideHooks.getLength() > 0) { + JSRunnable r = (JSRunnable)backSideHooks.shift(); + r.run(); + } + } + + private static int safariBacksideHookDelay; + + private static int safariBacksideHookDelay() { + if (safariBacksideHookDelay == 0) { + safariBacksideHookDelay = _safariBacksideHookDelay(); + if (safariBacksideHookDelay == 0) { + + // Based on my experiments, iOS 13 is far more forgiving for the backside hook + // delay time. So we set a default of 300, which seems to work. + // iOS 12 - not so forgiving. We set at 75. + if (!isIOS() || isIOS13()) { + // Desktop safari, and iOS devices on 13+ we give a 300 delay. + safariBacksideHookDelay = 300; + } else { + safariBacksideHookDelay = 75; + } + } + } + return safariBacksideHookDelay; + } + + @JSBody(params={}, script="var delay=window.getParameterByName('cn1SafariBacksideHookDelay'); if (delay) return parseInt(delay); return 0;") + private native static int _safariBacksideHookDelay(); + + private void installBacksideHooksInUserInteraction() { + if (isIOS() || isSafari()) { + debugLog("Installing backside hooks with delay "+safariBacksideHookDelay()); + runBacksideHooksInTimeout(safariBacksideHookDelay()); + } else { + startBacksideHooksInterval(); + runBacksideHooksInTimeout(300); + runBacksideHooksInTimeout(1500); + runBacksideHooksInTimeout(5000); + } + } + + /** + * Flag that is set on mousedown or touchstart in the cn1 canvas so that + * it knows to not pass events to rest of native peers until after mouseup/touchend + */ + + + + private class NativeOverlay { + HTMLInputElement el; + Component cmp; + NativeOverlay(Component cmp) { + this.cmp = cmp; + } + + void uninstall() { + if (el != null) { + window.getDocument().getBody().removeChild((HTMLInputElement)el); + } + } + + void update() { + + } + + void updateIfMovedAndFocused() { + + } + + void updateNativeEditorText(String text) { + + } + } + + @Override + public void updateNativeEditorText(Component c, String text) { + if (c.getNativeOverlay() != null) { + NativeOverlay o = (NativeOverlay)c.getNativeOverlay(); + o.updateNativeEditorText(text); + } + } + + + + private class TextAreaNativeOverlay extends NativeOverlay { + TextArea ta; + FocusListener focusListener; + DataChangedListener dataChangedListener; + boolean donePressed; + Thread monitorThread; + + @Override + void uninstall() { + super.uninstall(); + if (focusListener != null) { + ta.removeFocusListener(focusListener); + focusListener = null; + } + if (dataChangedListener != null && ta instanceof TextField) { + ((TextField)ta).removeDataChangedListener(dataChangedListener); + dataChangedListener = null; + } + } + + private void startMonitorThread() { + if (monitorThread == null) { + monitorThread = new Thread(new Runnable() { + public void run() { + while (jQuery_is_(inputEl, ":focus")) { + callSerially(new Runnable() { + + @Override + public void run() { + + } + + }); + } + } + }); + } + } + + + + + TextAreaNativeOverlay(TextArea taIn) { + super(taIn); + this.ta = taIn; + final HTMLInputElement inputEl; + if (!ta.isSingleLineTextArea()){ + inputEl = (HTMLInputElement)window.getDocument().createElement("textarea"); + isEditingSingleLine = true; + + } else { + inputEl = (HTMLInputElement)window.getDocument().createElement("input"); + inputEl.setType("text"); + isEditingSingleLine = false; + + } + + el = inputEl; + + inputEl.setAttribute("class", "cn1-edit-string"); + inputEl.getStyle().setProperty("outline", "none"); // for chrome + + String inputType = "text"; + if (ta.isSingleLineTextArea()) { + + switch (ta.getConstraint()) { + case TextArea.PASSWORD: + inputType = "password"; + break; + case TextArea.EMAILADDR: + inputType = "email"; + break; + case TextArea.NUMERIC: + inputType = "number"; + break; + case TextArea.PHONENUMBER: + inputType = "tel"; + break; + case TextArea.URL: + inputType = "url"; + break; + + } + inputEl.setAttribute("type", inputType); + + + } + + + + inputEl.addEventListener("keydown", new EventListener() { + + @Override + public void handleEvent(final Event evt) { + KeyEvent kevt = (KeyEvent)evt; + switch (kevt.getKeyCode()) { + case 9 : // tab + case 11 : // vertical tab + case 10 : // lf + case 13 : // cr + if (ta.isSingleLineTextArea() || kevt.getKeyCode() == 9 || kevt.getKeyCode() == 11) { + evt.preventDefault(); + evt.stopPropagation(); + } + break; + default: + + } + callSerially(new Runnable() { + public void run() { + final KeyEvent kevt = (KeyEvent)evt; + switch (kevt.getKeyCode()) { + case 9 : // tab + case 11 : // vertical tab + case 10 : // lf + case 13 : // cr + { + if (!ta.isSingleLineTextArea() && kevt.getKeyCode() != 9 && kevt.getKeyCode() != 11) { + // We don't do any special handling for multiline text fields. + return; + } + donePressed = true; + inputEl.blur(); + break; + } + } + + } + }); + } + + }); + + + focusListener = new FocusListener() { + + @Override + public void focusGained(Component cmpnt) { + + + if (!jQuery_is_(inputEl, ":focus")) { + inputEl.focus(); + } + Font f = ta.getSelectedStyle().getFont(); + if (f != null) { + NativeFont nf = (NativeFont)f.getNativeFont(); + inputEl.getStyle().setProperty("font",nf.getScaledCSS()); + } + inputEl.setValue(ta.getText()); + + } + + @Override + public void focusLost(Component cmpnt) { + if (jQuery_is_(inputEl, ":focus")) { + inputEl.blur(); + } + } + + }; + + ta.addFocusListener(focusListener); + + inputEl.addEventListener("input", new EventListener() { + + @Override + public void handleEvent(Event evt) { + callSerially(new Runnable() { + + @Override + public void run() { + String old = ta.getText(); + String value = inputEl.getValue(); + if (old == null && value != null || old != null && !old.equals(value)) { + ta.setText(value); + ta.repaint(); + } + } + + }); + } + + }, true); + + inputEl.addEventListener("focus", new EventListener() { + public void handleEvent(Event evt) { + callSerially(new Runnable() { + public void run() { + donePressed = false; + isEditing = true; + inputEl.getStyle().setProperty("color", HTML5Graphics.color(ta.getStyle().getFgColor())); + + Font f = ta.getSelectedStyle().getFont(); + if (f != null) { + NativeFont nf = (NativeFont)f.getNativeFont(); + inputEl.getStyle().setProperty("font",nf.getScaledCSS()); + } + inputEl.setValue(ta.getText()); + if (!ta.hasFocus()) { + ta.requestFocus(); + } + if (!ta.isEditing()) { + ta.startEditingAsync(); + } + ta.repaint(); + + UITimer.timer(500, false, new Runnable() { + + @Override + public void run() { + int vkbHeight = getScrollY_(); + Form current = _getCurrent(); + if (current != null &¤t.isFormBottomPaddingEditingMode()) { + + + current.getContentPane().getUnselectedStyle().setPaddingUnit(new byte[] {Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS}); + current.getContentPane().getUnselectedStyle().setPadding(Component.BOTTOM, unscaleCoord(vkbHeight)); + Window.current().scrollTo(0, 0); + + current.forceRevalidate(); + } + } + + }); + + + } + }); + } + }, true); + + inputEl.addEventListener("blur", new EventListener() { + public void handleEvent(Event evt) { + callSerially(new Runnable() { + public void run() { + isEditing = false; + inputEl.getStyle().setProperty("color", "transparent"); + ta.setText(inputEl.getValue()); + ta.repaint(); + Display.getInstance().onEditingComplete(ta, ta.getText()); + if (donePressed && ta instanceof TextArea) { + ((TextArea)ta).fireDoneEvent(); + } + donePressed = false; + + UITimer.timer(500, false, new Runnable() { + public void run() { + Form current = Display.getInstance().getCurrent(); + if (current != null && current.isFormBottomPaddingEditingMode()) { + current.getContentPane().getUnselectedStyle().setPaddingUnit(new byte[] {Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS}); + current.getContentPane().getUnselectedStyle().setPadding(Component.BOTTOM, 0); + current.forceRevalidate(); + } + } + }); + + } + }); + } + }, true); + + + + window.getDocument().getBody().appendChild(inputEl); + } + + int lastX; + int lastY; + int lastW; + int lastH; + + @Override + void updateIfMovedAndFocused() { + //if (ta.hasFocus()) { + int newX = ta.getAbsoluteX(); + int newY = ta.getAbsoluteY(); + int newW = ta.getWidth(); + int newH = ta.getHeight(); + if (lastX != newX || lastY != newY || lastW != newW || lastH != newH) { + lastX = newX; + lastY = newY; + lastW = newW; + lastH = newH; + update(); + } + //} + } + + @Override + void update() { + super.update(); + final HTMLInputElement inputEl = (HTMLInputElement)el; + CSSStyleDeclaration s = inputEl.getStyle(); + if (ta.isEditable() && ta.isVisible() && ta.getComponentForm().getComponentAt(ta.getAbsoluteX() + ta.getWidth()/2, ta.getAbsoluteY() + ta.getHeight()/2) == ta) { + // We only want to respond to pointer events if the text field is editable, visible, and is not covered by another component. + String propVal = s.getPropertyValue("pointer-events"); + if (!"auto".equals(propVal)) { + s.setProperty("pointer-events", "auto"); + } + inputEl.removeAttribute("disabled"); + } else { + String propVal = s.getPropertyValue("pointer-events"); + if (!"none".equals(propVal)) { + s.setProperty("pointer-events", "none"); + } + inputEl.setAttribute("disabled", "true"); + } + Style taStyle = cmp.getSelectedStyle(); + int paddingTop = taStyle.getPadding(Component.TOP); + int paddingLeft = taStyle.getPadding(ta.isRTL(), Component.LEFT); + int paddingRight = taStyle.getPadding(ta.isRTL(), Component.RIGHT); + int paddingBottom = taStyle.getPadding(Component.BOTTOM); + + s.setProperty("padding-top", scaleCoord((double)paddingTop)+"px"); + s.setProperty("padding-left", scaleCoord((double)paddingLeft)+"px"); + s.setProperty("padding-bottom", scaleCoord((double)paddingBottom)+"px"); + s.setProperty("padding-right", scaleCoord((double)paddingRight)+"px"); + + s.setProperty("display", "block"); + s.setProperty("top", scaleCoord((double)cmp.getAbsoluteY())+"px"); + s.setProperty("left", scaleCoord((double)cmp.getAbsoluteX())+"px"); + s.setProperty("width", scaleCoord((double)cmp.getWidth())+"px"); + s.setProperty("height", scaleCoord((double)cmp.getHeight())+"px"); + s.setProperty("border", "none"); + s.setProperty("margin", "0"); + } + + + } + + @Override + public void beforeComponentPaint(Component c, Graphics g) { + super.beforeComponentPaint(c, g); + Object overlay = c.getNativeOverlay(); + if (overlay != null) { + NativeOverlay no = (NativeOverlay)overlay; + no.updateIfMovedAndFocused(); + } + + } + + + + @Override + public Object createNativeOverlay(Component cmp) { + if (!useNativeOverlaysForTextFields()) { + // we only do this for phones and tablets + return null; + } + if (cmp instanceof TextArea) { + return new TextAreaNativeOverlay((TextArea)cmp); + } + return null; + } + + @Override + public void hideNativeOverlay(Component cmp, Object nativeOverlay) { + if (nativeOverlay != null) { + ((NativeOverlay)nativeOverlay).uninstall(); + } + } + + @Override + public void updateNativeOverlay(Component cmp, Object nativeOverlay) { + if (nativeOverlay != null) { + ((NativeOverlay)nativeOverlay).update(); + } + } + + private int currCursorType; + + private void setCursor(int cursorType) { + if (currCursorType != cursorType) { + currCursorType = cursorType; + String cursorStr = "default"; + switch (cursorType) { + case Component.HAND_CURSOR: + cursorStr = "pointer"; + break; + + case Component.DEFAULT_CURSOR: + cursorStr = "default"; + break; + case Component.NE_RESIZE_CURSOR: + cursorStr = "ne-resize"; + break; + case Component.NW_RESIZE_CURSOR: + cursorStr = "nw-resize"; + break; + case Component.W_RESIZE_CURSOR: + cursorStr = "w-resize"; + break; + case Component.E_RESIZE_CURSOR: + cursorStr = "e-resize"; + break; + case Component.N_RESIZE_CURSOR: + cursorStr = "n-resize"; + break; + case Component.S_RESIZE_CURSOR: + cursorStr = "s-resize"; + break; + case Component.MOVE_CURSOR: + cursorStr = "move"; + break; + case Component.CROSSHAIR_CURSOR: + cursorStr = "crosshair"; + break; + case Component.TEXT_CURSOR: + cursorStr = "text"; + break; + case Component.WAIT_CURSOR: + cursorStr = "wait"; + break; + case Component.SW_RESIZE_CURSOR: + cursorStr = "sw-resize"; + break; + case Component.SE_RESIZE_CURSOR: + cursorStr = "se-resize"; + break; + + + } + outputCanvas.getStyle().setProperty("cursor", cursorStr); + canvas.getStyle().setProperty("cursor", cursorStr); + peersContainer.getStyle().setProperty("cursor", cursorStr); + window.getDocument().getBody().getStyle().setProperty("cursor", cursorStr); + } + + + + } + + private int lastCanvasWidth; + private int lastCanvasHeight; + + public HTML5Implementation(){ + __init(); + } + private boolean inited; + + @JSBody(params={"evt"}, script="return ''+evt.detail") + private static native String getEventDetailString(Event evt); + + @JSBody(params={"evt"}, script="if (evt.code){return evt.code}else{return 0}") + private static native int getEventCode(Event evt); + + @JSBody(params={"message", "code"}, script="return new CustomEvent('cn1outbox', {detail:message, code:code});") + static native Event createCNOutboxEvent(String message, int code); + + + @JSBody(params={"type", "message", "code"}, script="return new CustomEvent(type, {detail:message, code:code});") + static native Event createCustomEvent(String type, String message, int code); + + @Override + public void postMessage(MessageEvent message) { + Event evt = createCNOutboxEvent(message.getMessage(), message.getCode()); + Window.current().dispatchEvent(evt); + } + + @JSBody(params={"evt", "mimeType"}, script="try {return evt.clipboardData.getData(mimeType)}catch(e){return ''}") + private native static String getPasteEventData(Event evt, String mimeType); + + @JSBody(params={"evt"}, script="try {return evt.clipboardData.files;} catch(e){return null}") + private native static FileList getPasteEventFileList(Event evt); + + private void firePasteEvent() { + callSerially(new Runnable() { + public void run() { + Form f = CN.getCurrentForm(); + if (f == null) { + return; + } + f.dispatchPaste(new ActionEvent(f)); + } + }); + } + + private void __init() { + if (inited) { + return; + } + inited = true; + instance=this; + window = Window.current(); + HTMLDocument document = window.getDocument(); + canvas = (HTMLCanvasElement)document.createElement("canvas"); + outputCanvas = (HTMLCanvasElement)document.getElementById("codenameone-canvas"); + outputCanvas.getStyle().setProperty("pointer-events", "none"); + peersContainer = (HTMLElement)document.createElement("div"); + peersContainer.setAttribute("id", "cn1-peers-container"); + outputCanvas.getParentNode().insertBefore(peersContainer, outputCanvas); + + nativeEdt = EasyThread.start("NativeEDT"); + + + //outputCanvas.getStyle().setProperty("opacity", "0.5"); + updateCanvasSize(); + defaultFont = (NativeFont)createFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM); + graphics = new BufferedGraphics(this, canvas); + + // Normalize browser locale + String blang = getBrowserLanguage(); + if (blang == null || blang.indexOf("-") == -1) { + blang = "en-US"; + } + String lang = blang.substring(0, blang.indexOf("-")); + String country = blang.substring(blang.indexOf("-")+1); + Locale.setDefault(new Locale(lang, country)); + final Display disp = Display.getInstance(); + String browserTz = null; + if ((browserTz = getProperty("browser.timezone", null)) != null) { + TimeZone.setDefault(TimeZone.getTimeZone(browserTz)); + //_log("Setting default timezone to "+TimeZone.getDefault().getDisplayName()); + } + + final EventListener cn1InboxListener = new EventListener() { + public void handleEvent(Event evt) { + final String detailString = getEventDetailString(evt); + final int eventCode = getEventCode(evt); + JavaScriptBrowserLifecycleCoordinator.handleInboxEvent(new JavaScriptBrowserLifecycleCoordinator.InboxHooks() { + public void run() { + } + + @Override + public void stopPropagation() { + evt.stopPropagation(); + } + + @Override + public void preventDefault() { + evt.preventDefault(); + } + + @Override + public void callSerially(Runnable runnable) { + HTML5Implementation.this.callSerially(runnable); + } + + @Override + public void dispatchMessage(String message, int code) { + Display.getInstance().dispatchMessage(new MessageEvent(CN.getCurrentForm(), message, code)); + } + }, detailString, eventCode); + } + }; + + final EventListener popstateListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + JavaScriptBrowserLifecycleCoordinator.handlePopState(new JavaScriptBrowserLifecycleCoordinator.BackNavigationHooks() { + @Override + public void callSerially(Runnable runnable) { + HTML5Implementation.this.callSerially(runnable); + } + + @Override + public void runBackCommand() { + Form f = getCurrentForm(); + if (f != null && f.getBackCommand() != null) { + f.getBackCommand().actionPerformed(new ActionEvent(f, ActionEvent.Type.Other)); + } + } + }); + } + + }; + + // Handle browser resizing. + final EventListener resizeListener = new EventListener(){ + + @Override + public void handleEvent(final Event evt) { + + callSerially(new Runnable(){ + + @Override + public void run() { + JavaScriptBrowserInteractionCoordinator.handleResize(new JavaScriptBrowserInteractionCoordinator.ResizeHooks() { + @Override + public void waitForResizeStabilization() { + CN.invokeAndBlock(new Runnable() { + @Override + public void run() { + Util.sleep(1); + } + }); + } + + @Override + public void updateCanvasSize() { + HTML5Implementation.this.updateCanvasSize(); + } + + @Override + public void sizeChanged() { + HTML5Implementation.this.sizeChanged(canvas.getWidth(), canvas.getHeight()); + } + + @Override + public void revalidate() { + HTML5Implementation.this.revalidate(); + } + }); + } + + }); + } + }; + final EventListener hoverListener = new EventListener() { + + @Override + public void handleEvent(Event evt) { + final MouseEvent me = (MouseEvent)evt; + new Thread() { + public void run() { + final int x = getClientX(me); + final int y = getClientY(me); + JavaScriptBrowserInteractionCoordinator.handleHover(new JavaScriptBrowserInteractionCoordinator.HoverHooks() { + @Override + public void dispatchHover(int x, int y) { + Display.getInstance().pointerHover(new int[]{x}, new int[]{y}); + } + + @Override + public void setCursor(int cursor) { + HTML5Implementation.this.setCursor(cursor); + } + + @Override + public void callSerially(Runnable runnable) { + Display.getInstance().callSerially(runnable); + } + }, new JavaScriptBrowserInteractionCoordinator.CursorLocator() { + @Override + public boolean isCursorEnabled() { + Form f = _getCurrent(); + return f != null && f.isEnableCursors(); + } + + @Override + public int resolveCursorAt(int x, int y) { + Form f = _getCurrent(); + if (f == null || x < 0 || x >= f.getWidth() || y < 0 || y >= f.getHeight()) { + return Component.DEFAULT_CURSOR; + } + Component cmp = f.getComponentAt(x, y); + return cmp != null ? cmp.getCursor() : Component.DEFAULT_CURSOR; + } + }, x, y, Component.DEFAULT_CURSOR); + } + }.start(); + } + + }; + + hitTest = new EventListener() { + @Override + public void handleEvent(Event evt) { + MouseEvent me = (MouseEvent)evt; + int x = getClientX(me); + int y = getClientY(me); + if (hitTest(x, y)) { + evt.preventDefault(); + evt.stopPropagation(); + } + } + + }; + + onPaste = new EventListener() { + @Override + public void handleEvent(Event evt) { + String plainText = getPasteEventData(evt, "text/plain"); + String htmlText = getPasteEventData(evt, "text/html"); + FileList files = getPasteEventFileList(evt); + String[] filePaths = null; + if (files != null) { + int len = files.getLength(); + filePaths = new String[len]; + for (int i=0; i 0 && paintNativePeersBehind(), hitTest(x, y)); + pointerState.setGrabbedDrag(routing.grabbedDrag()); + if (routing.shouldConsumeEvent()) { + evt.preventDefault(); + evt.stopPropagation(); + } + if (JavaScriptInputCoordinator.shouldIgnoreMousePress(pointerState.isTouchDown(), pointerState.isMouseDown(), evt.getTarget() == textField || evt.getTarget() == textArea)) { + debugLog("[mouseDown] touchIsDown"); + if (pointerState.isTouchDown()) { + pointerState.setMouseDown(false); + } + return; + } + onMouseMoveHandle = EventUtil.addEventListener(peersContainer, "mousemove", onMouseMove, true); + onPointerMoveHandle = EventUtil.addEventListener(peersContainer, "pointermove", onMouseMove, true); + + pointerState.setLastMousePosition(x, y); + pointerState.setMouseDown(true); + callSerially(new Runnable() { + public void run() { + + if (isEditing){ + finishTextEditing(); + } + } + }); + lastMouseEvent = me; + installBacksideHooksInUserInteraction(); + nativeCallSerially(new Runnable() { + public void run() { + HTML5Implementation.this.pointerPressed(new int[]{x}, new int[]{y}); + } + }); + if (contextListenerActive && me.getButton() == 2) { + contextListener.handleEvent(me); + } + + + + } + }; + + + + onMouseUp = new EventListener(){ + @Override + public void handleEvent(Event evt) { + if (nativeEventListener != null) { + CancelableEvent cevt = (CancelableEvent)evt; + nativeEventListener.handleEvent(evt); + if (cevt.isDefaultPrevented()) { + return; + } + } + debugLog("In mouseUp"); + MouseEvent me = (MouseEvent)evt; + final int x = getClientX(me) == -1 ? pointerState.getLastMouseX() : getClientX(me); + final int y = getClientY(me) == -1 ? pointerState.getLastMouseY() : getClientY(me); + focusInputElement(); + if (pointerState.isGrabbedDrag()) { + evt.preventDefault(); + evt.stopPropagation(); + } + pointerState.setGrabbedDrag(false); + + // Prevent conflicts with touch events + // Guard against mouseUp if the mouse isn't already dwon + if (pointerState.isTouchDown()) { + debugLog("[mouseUp] touchIsDown"); + pointerState.setMouseDown(false); + return; + } + + if (!pointerState.isMouseDown()) { + return; + } + pointerState.setMouseDown(false); + + + + EventUtil.removeEventListener(peersContainer, "mousemove", onMouseMoveHandle, true); + EventUtil.removeEventListener(peersContainer, "pointermove", onPointerMoveHandle, true); + + pointerState.setLastTouchUpPosition(x, y); + installBacksideHooksInUserInteraction(); + nativeCallSerially(new Runnable() { + public void run() { + HTML5Implementation.this.pointerReleased(new int[]{x}, new int[]{y}); + } + }); + callSerially(new Runnable() { + public void run() { + for (ActionListener l : mouseUpListeners) { + l.actionPerformed(null); + } + } + }); + + } + }; + + onTouchStart = new EventListener(){ + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + if (nativeEventListener != null) { + CancelableEvent cevt = (CancelableEvent)evt; + nativeEventListener.handleEvent(evt); + if (cevt.isDefaultPrevented()) { + return; + } + } + debugLog("In touchStart"); + TouchEvent me = (TouchEvent)evt; + JSArray touches = me.getTargetTouches(); + + int len = touches.getLength(); + final int[] x = new int[len]; + final int[] y = new int[len]; + + for (int i=0; i 0 && paintNativePeersBehind(), hitTest(x[0], y[0])); + pointerState.setGrabbedDrag(routing.grabbedDrag()); + if (routing.shouldConsumeEvent()) { + evt.preventDefault(); + evt.stopPropagation(); + } + JavaScriptInputCoordinator.TouchStartDecision touchDecision = JavaScriptInputCoordinator.resolveTouchStart( + pointerState.isMouseDown(), pointerState.isTouchDown(), evt.getTarget() == textField || evt.getTarget() == textArea, isEditing && editingStartingUp); + if (touchDecision.shouldIgnoreEvent()) { + return; + } + if (touchDecision.shouldCancelMouseTracking()) { + debugLog("[touchStart] mouseIsDown"); + pointerState.setMouseDown(false); + EventUtil.removeEventListener(peersContainer, "mousemove", onMouseMoveHandle, true); + EventUtil.removeEventListener(peersContainer, "pointermove", onPointerMoveHandle, true); + pointerState.setTouchDown(false); + } + pointerState.setTouchDown(true); + + + pointerState.setTouches(x, y); + + onTouchMoveHandle = EventUtil.addEventListener(peersContainer, "touchmove", onTouchMove, true); + + callSerially(new Runnable() { + + @Override + public void run() { + if (isEditing){ + if (currentEditingField != null) { + pendingTextChanges = currentEditingField.getText(); + } + if (!editingStartingUp) { + finishTextEditing(); + } else { + editingStartingUp = false; + } + } + } + }); + if (touchDecision.shouldFirePointerPressed()) { + installBacksideHooksInUserInteraction(); + nativeCallSerially(new Runnable() { + + @Override + public void run() { + HTML5Implementation.this.pointerPressed(x, y); + } + }); + } + + } + + }; + + + onTouchEnd = new EventListener(){ + + + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + if (nativeEventListener != null) { + CancelableEvent cevt = (CancelableEvent)evt; + nativeEventListener.handleEvent(evt); + if (cevt.isDefaultPrevented()) { + return; + } + } + debugLog("In TouchEnd"); + // Guard against mouse event conflicts + // Prevent from firing if touch was not down already. + if (JavaScriptInputCoordinator.shouldIgnoreTouchRelease(pointerState.isMouseDown(), pointerState.isTouchDown())) { + debugLog("[touchEnd] mouseIsDown"); + if (pointerState.isMouseDown()) { + pointerState.setTouchDown(false); + } + return; + } + pointerState.setTouchDown(false); + //if (evt.getTarget() == textField || evt.getTarget() == textArea) { + // // We don't want to respond to touch events on teh native input + // // fields because it can result in some infinite looping behaviour. + // return; + //} + focusInputElement(); + if (pointerState.isGrabbedDrag()) { + evt.preventDefault(); + evt.stopPropagation(); + } + pointerState.setGrabbedDrag(false); + + TouchEvent me = (TouchEvent)evt; + EventUtil.removeEventListener(peersContainer, "touchmove", onTouchMoveHandle, true); + installBacksideHooksInUserInteraction(); + nativeCallSerially(new Runnable() { + @Override + public void run() { + pointerState.setLastTouchUpPosition(pointerState.getTouchesX()[0], pointerState.getTouchesY()[0]); + HTML5Implementation.this.pointerReleased(pointerState.getTouchesX(), pointerState.getTouchesY()); + + } + }); + if (JavaScriptInputCoordinator.shouldCreatePreemptiveTextField(usePreemptiveNativeTextFieldApproach(), pointerState.getTouchStartTime(), currentTimeMillisecondsJS(), pointerState.getTouchStartX(), pointerState.getTouchStartY(), pointerState.getTouchesX()[0], pointerState.getTouchesY()[0])) { + // Hack for iOS only to anticipate clicking on a text field + createAndFocusTextFieldPreemptively(pointerState.getTouchesX()[0], pointerState.getTouchesY()[0]); + } + callSerially(new Runnable() { + @Override + public void run() { + for (ActionListener l : mouseUpListeners) { + l.actionPerformed(null); + } + } + }); + + } + + }; + + onTouchMove = new EventListener(){ + + @Override + public void handleEvent(Event evt) { + debugLog("in TouchMove"); + TouchEvent me = (TouchEvent)evt; + JSArray touches = me.getTargetTouches(); + + int len = touches.getLength(); + final int[] x = new int[len]; + final int[] y = new int[len]; + + for (int i=0; i 0 && paintNativePeersBehind()) { + + MouseEvent me = (MouseEvent)evt; + int x = unscaleCoord(me.getClientX()); + int y = unscaleCoord(me.getClientY()); + + if (!hitTest(x, y)) { + _debug("1.Failed hit test at "+x+","+y); + _debugObj(evt); + outputCanvas.getStyle().setProperty("pointer-events", "none"); + } else { + _debug("2. Passed hit test at "+x+","+y); + _debugObj(evt); + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + } + + } + } + + }, true); + + window.addEventListener(eventName, new EventListener() { + + @Override + public void handleEvent(Event evt) { + if (Accessor.getActivePeerCount() > 0 && paintNativePeersBehind() || + "none".equals(outputCanvas.getStyle().getPropertyValue("pointer-events"))) { + _debug("3. Restoring events"); + _debugObj(evt); + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + } + } + + }, false); + } + */ + /* + window.addEventListener("touchstart", new EventListener() { + + @Override + public void handleEvent(Event evt) { + if (Accessor.getActivePeerCount() > 0 && paintNativePeersBehind()) { + TouchEvent te = (TouchEvent)evt; + if (te.getTargetTouches().getLength() > 0) { + MouseEvent me = te.getTargetTouches().get(0); + int x = unscaleCoord(me.getClientX()); + int y = unscaleCoord(me.getClientY()); + boolean hitTestResult = false; + if (!hitTest(x, y)) { + if (pointerState.isCapturingEvents()) { + _debug("4. Failed hit test at "+x+","+y); + _debugObj(evt); + pointerState.setCapturingEvents(false); + outputCanvas.getStyle().setProperty("pointer-events", "none"); + outputCanvas.blur(); + evt.stopPropagation(); + evt.preventDefault(); + } + } else { + if (!pointerState.isCapturingEvents()) { + pointerState.setCapturingEvents(true); + + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + outputCanvas.focus(); + evt.stopPropagation(); + evt.preventDefault(); + } + + } + + + } + } else { + if (!pointerState.isCapturingEvents()) { + pointerState.setCapturingEvents(true); + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + outputCanvas.focus(); + evt.stopPropagation(); + evt.preventDefault(); + } + } + } + + }, true); + + window.addEventListener("mousedown", new EventListener() { + + @Override + public void handleEvent(Event evt) { + if (Accessor.getActivePeerCount() > 0 && paintNativePeersBehind()) { + + MouseEvent me = (MouseEvent)evt; + int x = unscaleCoord(me.getClientX()); + int y = unscaleCoord(me.getClientY()); + + if (!hitTest(x, y)) { + if (pointerState.isCapturingEvents()) { + pointerState.setCapturingEvents(false); + _debug("1.Failed hit test at "+x+","+y); + _debugObj(evt); + outputCanvas.getStyle().setProperty("pointer-events", "none"); + outputCanvas.blur(); + evt.stopPropagation(); + evt.preventDefault(); + + + } + } else { + if (!pointerState.isCapturingEvents()) { + pointerState.setCapturingEvents(true); + _debug("2. Passed hit test at "+x+","+y); + _debugObj(evt); + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + outputCanvas.focus(); + evt.stopPropagation(); + evt.preventDefault(); + } + } + + } else { + if (!pointerState.isCapturingEvents()) { + pointerState.setCapturingEvents(true); + outputCanvas.getStyle().setProperty("pointer-events", "auto"); + outputCanvas.focus(); + evt.stopPropagation(); + evt.preventDefault(); + } + + } + } + + }, true); + */ + final EventListener wheelListener = new EventListener() { + + @Override + public void handleEvent(final Event evt) { + MouseEvent me = (MouseEvent)evt; + final int x = getClientX(me); + final int y = getClientY(me); + if (hitTest(x, y)) { + evt.preventDefault(); + evt.stopPropagation(); + } + + new Thread() { + + @Override + public void run() { + mouseWheelMoved((WheelEvent)evt); + } + }.start(); + } + + }; + JavaScriptEventWiring.registerPeerPointerEvents(new JavaScriptEventWiring.ElementRegistrar() { + @Override + public void add(String eventName, Object listener, boolean capture) { + peersContainer.addEventListener(eventName, (EventListener) listener, capture); + } + }, !debugFlag("disableMousedown"), !debugFlag("disableMouseup"), !debugFlag("disableTouchstart"), + !debugFlag("disableTouchend"), !debugFlag("disableWheel"), getWheelEventType(), + onMouseDown, hitTest, onMouseUp, onTouchStart, onTouchEnd, wheelListener); + + /** + * The installbacksidehooks event is an event that can be triggered from native javascript to install + * backside hooks. This may be necessary if the user is interacting with the page outside of the app, or + * in a native widget - the interaction should consititute a user interaction, but because the touch event handler + * isn't triggered (where the backside hooks are usually installed). + * listener isn't being called, the + */ + final EventListener installBacksideHooksListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + JavaScriptBrowserLifecycleCoordinator.handleInstallBacksideHooks(new JavaScriptBrowserLifecycleCoordinator.BacksideHooks() { + @Override + public void installBacksideHooksInUserInteraction() { + HTML5Implementation.this.installBacksideHooksInUserInteraction(); + } + }); + } + + }; + + final EventListener keydownListener = new EventListener() { + + @Override + public void handleEvent(Event evt) { + final KeyEvent kevt = (KeyEvent) evt; + JavaScriptKeyboardInteractionAdapter.handleKeyDown(new JavaScriptKeyboardInteractionAdapter.EditingState() { + @Override + public boolean isEditing() { + return isEditing; + } + }, new JavaScriptKeyboardInteractionAdapter.BacksideHooks() { + @Override + public void installBacksideHooksInUserInteraction() { + HTML5Implementation.this.installBacksideHooksInUserInteraction(); + } + }, new JavaScriptKeyboardInteractionAdapter.KeyDispatch() { + @Override + public void preventDefault() { + evt.preventDefault(); + } + + @Override + public void nativeCallSerially(Runnable runnable) { + HTML5Implementation.this.nativeCallSerially(runnable); + } + + @Override + public void callSerially(Runnable runnable) { + HTML5Implementation.this.callSerially(runnable); + } + + @Override + public void setShiftKeyDown(boolean down) { + shiftKeyDown = down; + } + + @Override + public void setLastCharCode(int code) { + lastCharCode = code; + } + + @Override + public int translateKeyCode(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + return getCode(kevt); + } + + @Override + public void keyPressed(int code) { + HTML5Implementation.this.keyPressed(code); + } + + @Override + public void keyReleased(int code) { + } + + @Override + public void editFocusedTextArea(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + } + }, new JavaScriptKeyboardInteractionAdapter.KeyEventView() { + @Override + public int getKeyCode() { + return kevt.getKeyCode(); + } + + @Override + public int getCharCode() { + return kevt.getCharCode(); + } + + @Override + public boolean isShiftKey() { + return kevt.isShiftKey(); + } + }); + + } + + }; + + final EventListener keyupListener = new EventListener() { + + @Override + public void handleEvent(Event evt) { + final KeyEvent kevt = (KeyEvent) evt; + JavaScriptKeyboardInteractionAdapter.handleKeyUp(new JavaScriptKeyboardInteractionAdapter.BacksideHooks() { + @Override + public void installBacksideHooksInUserInteraction() { + HTML5Implementation.this.installBacksideHooksInUserInteraction(); + } + }, new JavaScriptKeyboardInteractionAdapter.KeyDispatch() { + @Override + public void preventDefault() { + } + + @Override + public void nativeCallSerially(Runnable runnable) { + HTML5Implementation.this.nativeCallSerially(runnable); + } + + @Override + public void callSerially(Runnable runnable) { + HTML5Implementation.this.callSerially(runnable); + } + + @Override + public void setShiftKeyDown(boolean down) { + shiftKeyDown = down; + } + + @Override + public void setLastCharCode(int code) { + lastCharCode = code; + } + + @Override + public int translateKeyCode(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + return getCode(kevt); + } + + @Override + public void keyPressed(int code) { + } + + @Override + public void keyReleased(int code) { + HTML5Implementation.this.keyReleased(code); + } + + @Override + public void editFocusedTextArea(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + } + }, new JavaScriptKeyboardInteractionAdapter.KeyEventView() { + @Override + public int getKeyCode() { + return kevt.getKeyCode(); + } + + @Override + public int getCharCode() { + return kevt.getCharCode(); + } + + @Override + public boolean isShiftKey() { + return kevt.isShiftKey(); + } + }); + + } + + }; + + final EventListener keypressListener = new EventListener() { + + @Override + public void handleEvent(Event evt) { + final KeyEvent kevt = (KeyEvent) evt; + JavaScriptKeyboardInteractionAdapter.handleKeyPress(new JavaScriptKeyboardInteractionAdapter.EditingState() { + @Override + public boolean isEditing() { + return isEditing; + } + }, new JavaScriptKeyboardInteractionAdapter.BacksideHooks() { + @Override + public void installBacksideHooksInUserInteraction() { + HTML5Implementation.this.installBacksideHooksInUserInteraction(); + } + }, new JavaScriptKeyboardInteractionAdapter.KeyDispatch() { + @Override + public void preventDefault() { + } + + @Override + public void nativeCallSerially(Runnable runnable) { + HTML5Implementation.this.nativeCallSerially(runnable); + } + + @Override + public void callSerially(Runnable runnable) { + HTML5Implementation.this.callSerially(runnable); + } + + @Override + public void setShiftKeyDown(boolean down) { + shiftKeyDown = down; + } + + @Override + public void setLastCharCode(int code) { + lastCharCode = code; + } + + @Override + public int translateKeyCode(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + return getCode(kevt); + } + + @Override + public void keyPressed(int code) { + } + + @Override + public void keyReleased(int code) { + } + + @Override + public void editFocusedTextArea(JavaScriptKeyboardInteractionAdapter.KeyEventView event) { + Form currentForm = Display.getInstance().getCurrent(); + if (currentForm != null) { + Component cmp = currentForm.getFocused(); + if (cmp != null && cmp instanceof TextArea) { + TextArea ta = (TextArea) cmp; + int charCode = event.getCharCode(); + switch (event.getKeyCode()) { + case 11: + case 9 : { // tab + if (event.isShiftKey()) { + cmp = currentForm.getPreviousComponent(cmp); + } else { + cmp = currentForm.getNextComponent(cmp); + } + + charCode = 0; + break; + } + /* + case 11 : { // vertical tab + if (kevt.isShiftKey()) { + cmp = currentForm.getNextFocusUp(); + } else { + cmp = currentForm.getNextFocusDown(); + } + cmp.requestFocus(); + charCode = 0; + break; + } + */ + case 10 : + case 13 : { // enter /new line + // Let's just let editString handle this for now. + } + } + + + if (cmp instanceof TextArea) { + ta = (TextArea) cmp; + } else { + //if (cmp != null) { + // cmp.requestFocus(); + // cmp.startEditingAsync(); + //} + return; + } + Display.getInstance().editString(cmp, ta.getMaxSize(), ta.getConstraint(), ta.getText(), charCode); + + } + + } + } + }, new JavaScriptKeyboardInteractionAdapter.KeyEventView() { + @Override + public int getKeyCode() { + return kevt.getKeyCode(); + } + + @Override + public int getCharCode() { + return kevt.getCharCode(); + } + + @Override + public boolean isShiftKey() { + return kevt.isShiftKey(); + } + }); + + } + }; + JavaScriptEventWiring.registerCoreWindowEvents(new JavaScriptEventWiring.WindowRegistrar() { + @Override + public void add(String eventName, Object listener, boolean capture) { + window.addEventListener(eventName, (EventListener) listener, capture); + } + }, !debugFlag("disableHover"), cn1InboxListener, popstateListener, resizeListener, hoverListener, + installBacksideHooksListener, keydownListener, keyupListener, keypressListener); + + animationFrameCallback = new AnimationFrameCallback(){ + + @SuppressSyncErrors + @Override + public void onAnimationFrame(int time) { + + if (graphicsLocked){ + // If the graphics is locked, we don't do anything + ((WindowExt)window).requestAnimationFrame(animationFrameCallback); + return; + + } + + JavaScriptRenderQueueState.FrameSnapshot frame = + JavaScriptRenderQueueCoordinator.beginFrame(new JavaScriptRenderQueueCoordinator.GraphicsLock() { + @Override + public void setGraphicsLocked(boolean locked) { + graphicsLocked = locked; + } + }, pendingDisplay); + + if (!frame.isEmpty()){ + CanvasRenderingContext2D context = (CanvasRenderingContext2D)outputCanvas.getContext("2d"); + //double ratio = getDevicePixelRatio(); + //if (ratio > 1.5) { + // context.scale(ratio, ratio); + //} + context.save(); + //context.setTransform(1, 0, 0, 1, 0, 0); + context.beginPath(); + context.rect(frame.getCropX(), frame.getCropY(), frame.getCropW(), frame.getCropH()); + context.clip(); + + for (ExecutableOp op : frame.getOps()){ + op.execute(context); + } + ClipRect.resetClip(context, graphics.getClipState()); + context.restore(); + + + } + ((WindowExt)window).requestAnimationFrame(animationFrameCallback); + + } + + }; + ((WindowExt)window).requestAnimationFrame(animationFrameCallback); + + } + + public static void callSerially(final Runnable r) { + new Thread() { + public void run() { + Display.getInstance().callSerially(r); + } + }.start(); + } + + @Override + protected int getDragAutoActivationThreshold() { + return 1000000; + } + + private boolean scrollWheeling; + + @Override + public boolean isScrollWheeling() { + return scrollWheeling; + } + + //@JSBody(script="return new Date.now()") + //private static native int currentTimeMillisecondsJS(); + private static long currentTimeMillisecondsJS() { + return System.currentTimeMillis(); + } + + private int getCode(KeyEvent evt) { + int code = evt.getKeyCode(); + if(code >= 'A' && code <= 'Z') { + int charCode = evt.getCharCode(); + if (charCode == 0) { + charCode = lastCharCode; + } + return charCode; + } + return code; + } + + /** + * @inheritDoc + */ + public int getClearKeyCode() { + return 46; + } + + /** + * @inheritDoc + */ + public int getBackspaceKeyCode() { + return 8; + } + + /** + * @inheritDoc + */ + public int getBackKeyCode() { + return 27; + } + + @Override + public void systemOut(String content) { + consoleLog(content); + } + + /** + * @inheritDoc + */ + public int getGameAction(int keyCode) { + switch (keyCode) { + case 38: + return Display.GAME_UP; + case 40: + return Display.GAME_DOWN; + case 39: + return Display.GAME_RIGHT; + case 37: + return Display.GAME_LEFT; + case 13: + return Display.GAME_FIRE; + } + return 0; + } + + /** + * @inheritDoc + */ + public int getKeyCode(int gameAction) { + switch (gameAction) { + case Display.GAME_UP: + return 38; + case Display.GAME_DOWN: + return 40; + case Display.GAME_RIGHT: + return 39; + case Display.GAME_LEFT: + return 37; + case Display.GAME_FIRE: + return 13; + } + return 0; + } + + + public void nativeCallSerially(final Runnable r) { + if (!debugFlag("useNativeQueue")) { + new Thread(new Runnable() { + @Override + public void run() { + nativeEdt.run(r); + } + }).start(); + } else { + new Thread(r).start(); + } + } + + @JSBody(params={}, script="return window.cn1WheelMultiplier || 1.0") + private static native double wheelMultiplier(); + + public void mouseWheelMoved(WheelEvent e) { + NormalizedWheelEvent ne = normalizeWheelEvent(e); + + //e.preventDefault(); + //if (!isEnabled()) { + // return; + //} + final int x = getClientX(e); + final int y = getClientY(e); + //debugLog("in mouseWheelMoved at ("+x+","+y+")"); + //if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) { + //requestFocus(); + //final int units = convertToPixels(e.getUnitsToScroll() * 5, true) * -1; + //final int dx = -(int)(e.getDeltaX() * getDevicePixelRatio() * wheelMultiplier()); + //final int dy = -(int)(e.getDeltaY() * getDevicePixelRatio() * wheelMultiplier()); + final int dx = -(int)(ne.getPixelX() * getDevicePixelRatio() * wheelMultiplier()); + final int dy = -(int)(ne.getPixelY() * getDevicePixelRatio() * wheelMultiplier()); + //debugLog("dx="+dx+"; dy="+dy); + Display.getInstance().callSerially(new Runnable() { + public void run() { + scrollWheeling = true; + Form f = getCurrentForm(); + if(f != null){ + //Component cmp = f.getContentPane().getComponentAt(x, y); + Component cmp = f.getComponentAt(x, y); + if(cmp != null && cmp.isFocusable()) { + cmp.setFocusable(false); + f.pointerPressed(x, y); + f.pointerDragged(x + dx, y + dy / 4); + cmp.setFocusable(true); + } else { + f.pointerPressed(x, y); + f.pointerDragged(x + dx, y + dy / 4); + } + } + } + }); + Display.getInstance().callSerially(new Runnable() { + public void run() { + Form f = getCurrentForm(); + if(f != null){ + //Component cmp = f.getContentPane().getComponentAt(x, y); + Component cmp = f.getComponentAt(x, y); + if(cmp != null && cmp.isFocusable()) { + cmp.setFocusable(false); + f.pointerDragged(x + dx, y + dy / 4 * 2); + cmp.setFocusable(true); + } else { + f.pointerDragged(x + dx, y + dy / 4 * 2); + } + } + } + }); + Display.getInstance().callSerially(new Runnable() { + public void run() { + Form f = getCurrentForm(); + if(f != null){ + //Component cmp = f.getContentPane().getComponentAt(x, y); + Component cmp = f.getComponentAt(x, y); + if(cmp != null && cmp.isFocusable()) { + cmp.setFocusable(false); + f.pointerDragged(x + dx, y + dy / 4 * 3); + cmp.setFocusable(true); + } else { + f.pointerDragged(x + dx, y + dy / 4 * 3); + } + } + } + }); + Display.getInstance().callSerially(new Runnable() { + public void run() { + Form f = getCurrentForm(); + if(f != null){ + //Component cmp = f.getContentPane().getComponentAt(x, y); + Component cmp = f.getComponentAt(x, y); + if(cmp != null && cmp.isFocusable()) { + cmp.setFocusable(false); + f.pointerDragged(x + dx, y + dy); + f.pointerReleased(x + dx, y + dy); + cmp.setFocusable(true); + } else { + f.pointerDragged(x + dx, y + dy); + f.pointerReleased(x + dx, y + dy); + } + } + scrollWheeling = false; + } + }); + + } + + private void updateCanvasSize() { + JavaScriptCanvasLayout.Dimensions dimensions = JavaScriptCanvasLayout.compute( + window.getDocument().getBody().getClientWidth(), + window.getInnerHeight(), + getDevicePixelRatio()); + canvas.setWidth(dimensions.getBackingWidth()); + canvas.setHeight(dimensions.getBackingHeight()); + outputCanvas.setWidth(dimensions.getBackingWidth()); + outputCanvas.setHeight(dimensions.getBackingHeight()); + peersContainer.getStyle().setProperty("height", dimensions.getCssHeight() + "px"); + peersContainer.getStyle().setProperty("width", dimensions.getCssWidth() + "px"); + if (dimensions.getStyleWidth() != null) { + outputCanvas.getStyle().setProperty("width", dimensions.getStyleWidth()); + outputCanvas.getStyle().setProperty("height", dimensions.getStyleHeight()); + canvas.getStyle().setProperty("width", dimensions.getStyleWidth()); + canvas.getStyle().setProperty("height", dimensions.getStyleHeight()); + } + } + + public void revalidate() { + Form f = getCurrentForm(); + if (f != null){ + f.revalidate(); + + flushGraphics(); + } + + } + + public static void setMainClass(Object main) { + JavaScriptBootstrapCoordinator.bindMainClass(main, + new JavaScriptBootstrapCoordinator.PushCallbackRegistrar() { + @Override + public void register(PushCallback callback) { + setPushCallback(callback); + } + }, + new JavaScriptBootstrapCoordinator.PushCallbackRegistrar() { + @Override + public void register(PushCallback callback) { + HTML5Push.setPushCallback(callback); + } + }); + } + + public MouseEvent getLastMouseEvent() { + return lastMouseEvent; + } + + @Override + public void init(Object m) { + //__init(); + if (m instanceof Runnable) { + //((Runnable)m).run(); + } + if (JavaScriptInputCoordinator.shouldInstallKeyboard(isPhoneOrTablet_())) { + HTML5Keyboard.install(); + } + // Set the locale + installBeforeUnload(); + + + Font.setDefaultFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM)); + Display d = Display.getInstance(); + final WindowExt win = (WindowExt)Window.current(); + final Navigator navigator = win.getNavigator(); + JavaScriptRuntimeEnvironment environment = createRuntimeEnvironment(win, navigator); + JavaScriptInitializationAdapter.applyEnvironment(new JavaScriptInitializationAdapter.PropertySink() { + @Override + public void setProperty(String key, String value) { + d.setProperty(key, value); + } + }, environment); + setAppArg(JavaScriptInitializationAdapter.resolveAppArg(environment)); + JavaScriptInitializationAdapter.runPostInit(new JavaScriptInitializationAdapter.RuntimeHooks() { + @Override + public void setDragStartPercentage(int percentage) { + HTML5Implementation.this.setDragStartPercentage(percentage); + } + + @Override + public void initVideoCaptureConstraints() { + // On iOS we can't really use capture constraints yet anyways + // https://github.com/collab-project/videojs-record/issues/181 + // https://github.com/collab-project/videojs-record/issues/332 + VideoCaptureConstraints.init(new JSVideoCaptureConstraintsCompiler()); + } + + @Override + public void registerSaveBlobToFile() { + HTML5Implementation.registerSaveBlobToFile(); + } + + @Override + public void initGoogle() { + GoogleImpl.init(); + } + }, isIOS()); + } + + private JavaScriptRuntimeEnvironment createRuntimeEnvironment(final WindowExt win, final Navigator navigator) { + return new JavaScriptRuntimeEnvironment( + navigator.getPlatform(), + navigator.getUserAgent(), + navigator.getLanguage(), + navigator.getAppName(), + navigator.getAppCodeName(), + navigator.getAppVersion(), + win.getCN1DeploymentType(), + ((WindowLocation)Window.current().getLocation()).getHref() + ); + } + + @JSBody(params={"msg"}, script="window.onbeforeunload=function(){return msg;}") + private native static void setBeforeUnloadMessage(String msg); + + @JSBody(params={}, script="window.onbeforeunload=function(){}") + private native static void removeBeforeUnload(); + + @Override + public void setPlatformHint(String key, String value) { + if ("platformHint.javascript.beforeUnloadMessage".equalsIgnoreCase(key)) { + if (value == null) { + removeBeforeUnload(); + } else { + setBeforeUnloadMessage(value); + } + } + if ("platformHint.javascript.backsideHooksInterval".equalsIgnoreCase(key)) { + if (value == null) { + value = "0"; + } + int intVal = Integer.parseInt(value); + if (intVal < 0) { + if (backsideHooksIntervalHandle != 0) { + Window.clearInterval(backsideHooksIntervalHandle); + backsideHooksIntervalHandle = 0; + } + } + backsideHooksIntervalTimeout = intVal; + } + } + + + + @Override + public String getProperty(String key, String defaultValue) { + Window win = (Window)Window.current(); + + //Display.getInstance().getProperty("os.gzip", "false") + if ("os.gzip".equals(key)) { + // Flag used to indicate that the browser takes care of GZipped + // connection responses seamlessly so GZIPInputStream doesn't + // need to do anything. + return "true"; + } + if ("browser.window.location.href".equals(key)) { + return ((WindowLocation)Window.current().getLocation()).getHref(); + } + if ("browser.window.location.search".equals(key)) { + return Window.current().getLocation().getSearch(); + } + if ("browser.window.location.host".equals(key)) { + return Window.current().getLocation().getHost(); + } + if ("browser.window.location.hash".equals(key)) { + return Window.current().getLocation().getHash(); + } + if ("browser.window.location.origin".equals(key)) { + return ((WindowLocation)Window.current().getLocation()).getOrigin(); + } + if ("browser.window.location.pathname".equals(key)) { + return ((WindowLocation)Window.current().getLocation()).getPathname(); + } + if ("browser.window.location.protocol".equals(key)) { + return Window.current().getLocation().getProtocol(); + } + if ("browser.window.location.port".equals(key)) { + return Window.current().getLocation().getPort(); + } + if ("browser.window.location.hostname".equals(key)) { + return ((WindowLocation)Window.current().getLocation()).getHostname(); + } + if ("browser.timezone".equals(key)) { + DateTimeZone tz = DateTimeZoneProvider.detectTimezone(); + if (tz != null) { + return tz.getID(); + } else { + return defaultValue; + } + } + if ("HTML5.platformName".equals(key)) { + if (isAndroid_()) { + return "and"; + } else if (isIOS()) { + return "ios"; + } else if (isMac()) { + return "mac"; + } else { + return "win"; + } + } + return super.getProperty(key, defaultValue); + } + + + + + + private HTMLCanvasElement getCanvasBuffer(int width, int height){ + scratchBuffer = JavaScriptCanvasImageBufferLifecycle.ensureScratchBuffer(scratchBuffer, width, height, + new JavaScriptCanvasImageBufferLifecycle.ScratchCanvasFactory() { + @Override + public HTMLCanvasElement createScratchCanvas() { + return (HTMLCanvasElement)window.getDocument().createElement("canvas"); + } + }, new JavaScriptCanvasImageBufferLifecycle.CanvasSizeAccess() { + @Override + public int getWidth(HTMLCanvasElement canvas) { + return canvas.getWidth(); + } + + @Override + public int getHeight(HTMLCanvasElement canvas) { + return canvas.getHeight(); + } + + @Override + public void setWidth(HTMLCanvasElement canvas, int canvasWidth) { + canvas.setWidth(canvasWidth); + } + + @Override + public void setHeight(HTMLCanvasElement canvas, int canvasHeight) { + canvas.setHeight(canvasHeight); + } + }); + return scratchBuffer; + } + + public void installNativeTheme(){ + try { + String nativeTheme = Display.getInstance().getProperty("javascript.native.theme", isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res"); + + Resources r = Resources.open(nativeTheme); + Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]); + + tp.put("StatusBar.padding", "0,0,0,0"); + + UIManager.getInstance().setThemeProps(tp); + return; + } catch (IOException ex){ + Log.e(ex); + } + return; + } + + @Override + public boolean hasNativeTheme() { + return true; + } + + private int isDesktop = -1; + + @Override + public boolean isDesktop() { + + if (isDesktop == -1) { + String overrideVal = getParameterByName("isDesktop"); + if ("1".equals(overrideVal)) { + isDesktop = 1; + } else if ("0".equals(overrideVal)) { + isDesktop = 0; + } else { + isDesktop = isPhoneOrTablet_() ? 0:1; + } + } + return isDesktop==1; + } + + + @JSBody(params={"name"}, script="return window.cn1_debug_flags && window.cn1_debug_flags[name];" ) + private native static boolean debugFlag(String name); + + /** + * Returns true if this is a mobile browser - and not a tablet. + * @return + */ + // From http://stackoverflow.com/a/11381730/2935174 + @JSBody(params={}, script="var a = navigator.userAgent||navigator.vendor||window.opera; " + + "return /(android|bb\\d+|meego).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-/i.test(a.substr(0,4));") + private native static boolean isPhone_(); + + @JSBody(params={}, script="var a = navigator.userAgent||navigator.vendor||window.opera;" + + "return /(android|bb\\d+|meego).+mobile|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-/i.test(a.substr(0,4));") + native static boolean isPhoneOrTablet_(); + + @JSBody(script="return (navigator.userAgent.toLowerCase().indexOf(\"android\") > -1)") + public native static boolean isAndroid_(); + + @Override + public int getDeviceDensity() { + if (dDensity == -1) { + int overrideDensity = getDensityOverride(); + if (overrideDensity > 0) { + dDensity = overrideDensity; + return dDensity; + } + double ratio = getDevicePixelRatio(); + if (isPhone_()) { + // This is a phone + if (ratio < 2) { + dDensity = Display.DENSITY_MEDIUM; + } else if (ratio < 2.5) { + dDensity = Display.DENSITY_VERY_HIGH; + } else if (ratio < 4) { + dDensity = Display.DENSITY_HD; + } else if (ratio < 5) { + dDensity = Display.DENSITY_560; + } else if (ratio < 7) { + dDensity = Display.DENSITY_2HD; + } else { + dDensity = Display.DENSITY_4K; + } + } else if (isPhoneOrTablet_()) { + // It must be a tablet + if (ratio < 1.9) { + dDensity = Display.DENSITY_MEDIUM; + } else { + dDensity = Display.DENSITY_VERY_HIGH; + } + + } else { + // We're dealing with a desktop computer. + // Based on the Retina models here (https://en.wikipedia.org/wiki/Retina_Display) + // The display density on desktops are much lower. Retina displays (which would have a + // pixel ratio of 2, would generally be about 220ppi. Our High density is about 240ppi + // So that puts the property density for a pixel ratio of over 2 at about HIGH. + // Regular pixel density would correspond with about half that. Our LOW density + // is about 120ppi which fits the bill + if (ratio < 1.9) { + dDensity = Display.DENSITY_MEDIUM; + } else if (ratio < 2.9) { + dDensity = Display.DENSITY_VERY_HIGH; + } else { + dDensity = Display.DENSITY_HD; + } + } + + + } + return dDensity; + + } + + + private static interface NormalizedWheelEvent extends JSObject { + @JSProperty + double getPixelX(); + + @JSProperty + double getPixelY(); + + @JSProperty + double getSpinX(); + + @JSProperty + double getSpinY(); + } + + @JSBody(params={"evt"}, script="return window.cn1NormalizeWheel(evt)") + private native static NormalizedWheelEvent normalizeWheelEvent(WheelEvent evt); + + @JSBody(params={}, script="return window.cn1NormalizeWheel.getEventType()") + public native static String getWheelEventType(); + + private double ppi=0; + private int dDensity = -1; + + @Override + public int convertToPixels(int dipCount, boolean horizontal) { + if (ppi == 0) { + switch (getDeviceDensity()) { + case Display.DENSITY_VERY_LOW : + ppi = 72.0 / 25.4; break; + case Display.DENSITY_LOW : + ppi = 120.0 / 25.4; break; + case Display.DENSITY_MEDIUM: + ppi = 160.0 / 25.4; break; + case Display.DENSITY_HIGH: + ppi = 240.0 / 25.4; break; + case Display.DENSITY_VERY_HIGH: + ppi = 320.0 / 25.4; break; + case Display.DENSITY_HD: + ppi = 540.0 / 25.4; break; + case Display.DENSITY_560: + ppi = 750.0 / 25.4; break; + case Display.DENSITY_2HD: + ppi = 1080.0 / 25.4; break; + case Display.DENSITY_4K: + ppi = 1280.0 /25.4; break; + default: + ppi = 160.0 / 25.4; break; + } + } + return (int)Math.round((((float)dipCount) * ppi)); + } + + @JSBody(params={}, script="var ua = navigator.userAgent.toLowerCase(); \n" + + "if (ua.indexOf('safari') != -1) { \n" + + " if (ua.indexOf('chrome') > -1) {\n" + + " return false;\n" + + " } else {\n" + + " return true;\n" + + " }\n" + + "}\n" + + "return false;") + private static native boolean _isSafari(); + private static boolean isSafari; + private static boolean isSafariChecked; + public static boolean isSafari() { + if (!isSafariChecked) { + isSafariChecked = true; + isSafari = _isSafari(); + } + return isSafari; + } + + @JSBody(params={}, script="return (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)") + public static native boolean isIOS(); + + @JSBody(params={}, script="return /Mac/.test(navigator.userAgent)") + public static native boolean isMac(); + + // From https://stackoverflow.com/questions/57599945/how-to-detect-ios-13-on-javascript + @JSBody(params={}, script="return \"download\" in document.createElement(\"a\")") + private static native boolean doesAnchorSupportDownload(); + + private static boolean isIOS13() { + return isIOS() && doesAnchorSupportDownload(); + } + + @JSBody(params={}, script="return (navigator.userAgent.match(/iPad/i) != null) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)") + private static native boolean isIPad(); + + + @JSBody(params={}, script="if (window.overridePixelRatio === undefined) {" + + " var ratioStr = getParameterByName('pixelRatio');" + + " if (ratioStr != '') {" + + " window.overridePixelRatio = parseFloat(ratioStr);" + + " } else {" + + " window.overridePixelRatio = 0;" + + " }" + + " if (window.cn1ScaleCoord === undefined){ window.cn1ScaleCoord = function(x) { return x===-1?-1:x/(window.overridePixelRatio || window.devicePixelRatio || 1.0);};}" + + " if (window.cn1UnscaleCoord === undefined){ window.cn1UnscaleCoord = function(x) { return x===-1?-1:x*(window.overridePixelRatio || window.devicePixelRatio || 1.0);};}" + + "}" + + "return window.overridePixelRatio || window.devicePixelRatio || 1.0") + static native double getDevicePixelRatio_(); + + + @JSBody(params={"name"}, script="return getParameterByName(name);") + static native String getParameterByName(String name); + + static double getDevicePixelRatio() { + if (devicePixelRatio < 0) { + devicePixelRatio = getDevicePixelRatio_(); + } + return devicePixelRatio; + } + + @Override + public int getDisplayWidth() { + return canvas.getWidth(); + } + + @Override + public int getDisplayHeight() { + return canvas.getHeight(); + } + + @Override + public boolean isNativeInputSupported() { + return true; + } + + @Override + public boolean isNativeInputImmediate() { + return true; + } + + @JSBody(params={"ext"}, script="switch (ext) {" + + "case 'aac': return 'audio/x-aac';" + + "case 'aif': return 'audio/x-aiff';" + + "case 'uva': return 'audio/vnd.dece.audio';" + + "case 'm3u': return 'audio/x-mpegurl';" + + "case 'wma': return 'audio/x-ms-wma';" + + "case 'mid': return 'audio/midi';" + + "case 'mpga': return 'audio/mpeg';" + + "case 'mp4a': return 'audio/mp4';" + + "case 'mp3': return 'audio/mp3';" + + "case 'oga': return 'audio/ogg';" + + "case 'weba': return 'audio/webm';" + + "case 'wav': return 'audio/x-wav';" + + "case '3gp': return 'video/3gpp';" + + "case '3g2': return 'video/3gpp2';" + + "case 'avi': return 'video/x-msvideo';" + + "case 'f4v': return 'video/x-f4v';" + + "case 'flv': return 'video/xflv';" + + "case 'h261': return 'video/h261';" + + "case 'h263': return 'video/h263';" + + "case 'h264': return 'video/h264';" + + "case 'jpgv': return 'video/jpeg';" + + "case 'm4v': return 'video/x-m4v';" + + "case 'asf': return 'video/x-ms-asf';" + + "case 'wm': return 'video/x-ms-wm';" + + "case 'wmx': return 'video/x-mws-wmx';" + + "case 'wmv': return 'video/x-ms-wmv';" + + "case 'mpeg': return 'video/mpeg';" + + "case 'mp4': return 'video/mp4';" + + "case 'ogv': return 'video/ogg';" + + "case 'webm': return 'video/webm';" + + "case 'qt': return 'video/quicktime';" + + "default: return 'video/mp4';" + + "}") + private static native String getMimeForExtension(String ext); + + private static String guessMime(String uri) { + if (isTempFile(uri)) { + Blob tmpFile = getTempFile(uri); + if (tmpFile != null && tmpFile.getType() != null && !"".equals(tmpFile.getType())) { + return tmpFile.getType(); + } + } + if (uri.indexOf("#") > 0) { + uri = uri.substring(0, uri.indexOf("#")); + } + if (uri.indexOf("?") > 0) { + uri = uri.substring(0, uri.indexOf("?")); + } + if (uri.lastIndexOf(".") > 0) { + String ext = uri.substring(uri.lastIndexOf(".")+1); + return getMimeForExtension(ext); + + } + + return "application/octet-stream"; + + } + + @Override + public Media createMedia(String uri, boolean isVideo, final Runnable onCompletion) throws IOException { + return createMedia(uri, isVideo, null, onCompletion); + } + + @Override + public boolean isNativeVideoPlayerControlsIncluded() { + return true; + } + + @Override + public AsyncResource createMediaAsync(InputStream stream, String mimeType, Runnable onCompletion) { + final AsyncResource out = new AsyncResource(); + final HTML5Media media; + try { + media = (HTML5Media)createMedia(stream, mimeType, onCompletion, false); + } catch (IOException ex) { + out.error(ex); + return out; + } + final boolean[] handled = new boolean[1]; + final HTMLMediaElement el = media.getMediaElement(); + final EventListener errorListener = new EventListener() { + @Override + public void handleEvent(final Event evt) { + _logObj(((org.teavm.jso.dom.html.HTMLMediaElement)evt.getTarget()).getError()); + new Thread() { + public void run() { + _log("error event received loading stream"); + + if (handled[0]) { + Log.p("WARNING: error event called after load events already handled when loading media from stream "); + return; + } + handled[0] = true; + out.error(new IOException("Failed to load media from stream")); + } + }.start(); + } + + }; + el.addEventListener("error", errorListener); + mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + @Override + public void run(HTMLElement thisEl) { + thisEl.removeEventListener("error", errorListener); + } + }); + final EventListener loadedMetadataListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + new Thread() { + public void run() { + _log("loadstart event received loading stream"); + if (handled[0]) { + Log.p("WARNING: loadstart event called after load events already handled when loading media from stream "); + return; + } + handled[0] = true; + out.complete(media); + } + }.start(); + } + + }; + el.addEventListener("loadedmetadata", loadedMetadataListener); + mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + @Override + public void run(HTMLElement thisEl) { + thisEl.removeEventListener("loadedmetadata", loadedMetadataListener); + } + + }); + + return out; + } + + + + @Override + public AsyncResource createMediaAsync(final String uri, boolean video, Runnable onCompletion) { + final AsyncResource out = new AsyncResource(); + final HTML5Media media; + try { + media = (HTML5Media)createMedia(uri, video, null, onCompletion, false); + } catch (IOException ex) { + out.error(ex); + return out; + } + final boolean[] handled = new boolean[1]; + final HTMLMediaElement el = media.getMediaElement(); + final EventListener errorListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + _logObj(((org.teavm.jso.dom.html.HTMLMediaElement)evt.getTarget()).getError()); + new Thread() { + public void run() { + _log("error event received loading stream"); + _log(uri); + if (handled[0]) { + Log.p("WARNING: error event called after load events already handled when loading media from uri "+uri); + return; + } + handled[0] = true; + out.error(new IOException("Failed to load media from "+uri)); + } + }.start(); + } + + }; + el.addEventListener("error", errorListener); + final EventListener loadedMetaDataListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + new Thread() { + public void run() { + _log("loadstart received loading stream"); + _log(uri); + if (handled[0]) { + Log.p("WARNING: loadstart event called after load events already handled when loading media from uri "+uri); + return; + } + handled[0] = true; + out.complete(media); + } + }.start(); + } + + }; + el.addEventListener("loadedmetadata", loadedMetaDataListener); + mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + @Override + public void run(HTMLElement thisEl) { + thisEl.removeEventListener("error", errorListener); + thisEl.removeEventListener("loadedmetadata", loadedMetaDataListener); + } + }); + return out; + } + + + private HTML5MediaPool mediaPool; + + HTML5MediaPool mediaPool() { + if (mediaPool == null) { + mediaPool = new HTML5MediaPool(); + } + return mediaPool; + } + + private Media createMedia(String uri, boolean isVideo, String mime, final Runnable onCompletion) throws IOException { + return createMedia(uri, isVideo, mime, onCompletion, true); + } + + + + private Media createMedia(String uri, boolean isVideo, String mime, final Runnable onCompletion, boolean blocking) throws IOException { + //_log("Creating media for "+uri); + HTMLMediaElement el = null; + if (isVideo){ + el = mediaPool().createVideoElement(); + } else { + el = mediaPool().createAudioElement(); + } + + mime = mime == null ? guessMime(uri) : mime; + if (isVideo && (mime == null || !mime.startsWith("video"))) { + mime = "video/mp4"; + } + if (!isVideo && (mime == null || !mime.startsWith("audio"))) { + mime = "audio/wav"; + } + FileSystemStorage fs = FileSystemStorage.getInstance(); + //if (uri.indexOf("file://") == 0) { + if (fs.exists(uri)) { + mediaPool().returnMediaElement(el); + _log("Opening media from file system "+uri); + return createMedia(this.openFileInputStream(uri), mime, onCompletion, blocking); + //ArrayBufferInputStream is = (ArrayBufferInputStream)this.openFileInputStream(uri); + //String dataURL = arrayBufferToDataURL(is.getBuffer().getBuffer(), mime); + //el.setSrc(dataURL); + } else if (uri.indexOf("//") >= 0 || uri.indexOf("data:") == 0 || uri.indexOf("assets/") == 0) { + _log("Opening media from uri "+uri); + el.setSrc(uri); + } else { + _log("Opening media from resource stream "+uri); + ArrayBufferInputStream is = (ArrayBufferInputStream)this.getResourceAsStream(null, uri); + String dataURL = arrayBufferToDataURL(is.getBuffer().getBuffer(), mime); + el.setSrc(dataURL); + } + if (blocking) { + final boolean[] error = new boolean[1]; + final boolean[] complete = new boolean[1]; + final EventListener errorListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + _logObj(((org.teavm.jso.dom.html.HTMLMediaElement)evt.getTarget()).getError()); + new Thread() { + public void run() { + synchronized(complete) { + error[0] = true; + complete[0] = true; + complete.notify(); + } + } + }.start(); + } + + }; + + el.addEventListener("error", errorListener); + mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + public void run(HTMLElement theEl) { + theEl.removeEventListener("error", errorListener); + } + }); + final EventListener loadedMetaDataListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + new Thread() { + public void run() { + synchronized(complete) { + complete[0] = true; + complete.notify(); + } + } + }.start(); + + } + + }; + el.addEventListener("loadedmetadata", loadedMetaDataListener); + mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + public void run(HTMLElement theEl) { + theEl.removeEventListener("loadedmetadata", loadedMetaDataListener); + } + }); + while (!complete[0]) { + invokeAndBlock(new Runnable() { + public void run() { + synchronized(complete) { + Util.wait(complete); + } + } + + }); + } + if (error[0]) { + throw new IOException("Failed to load media from uri "+uri); + } + } + HTML5Media out = new HTML5Media(el, isVideo); + if (onCompletion!=null){ + out.addCompletionHandler(onCompletion); + } + + + return out; + + } + + @Override + public void addCompletionHandler(Media media, Runnable onCompletion) { + + super.addCompletionHandler(media, onCompletion); + if (media instanceof HTML5Media) { + ((HTML5Media)media).addCompletionHandler(onCompletion); + } + } + + @Override + public void removeCompletionHandler(Media media, Runnable onCompletion) { + if (media instanceof HTML5Media) { + ((HTML5Media)media).removeCompletionHandler(onCompletion); + } + super.removeCompletionHandler(media, onCompletion); + } + + + + @Override + public Media createMedia(InputStream stream, String mimeType, Runnable onCompletion) throws IOException { + return createMedia(stream, mimeType, onCompletion, true); + } + + /** + * Creates media. + * @param stream InputStream to create media for + * @param mimeType Mimetype of the media. May be null. + * @param onCompletion Callback to run on completion of playing media. + * @param blocking Whether to use invokeAndBlock to block return until the media has either errored, or started loading. Normal calls + * to createMedia block so that errors result in an IOException being thrown. Async wrappers will call this method with blocking=false + * to that they can handle errors and returns asynchronously, without underlying baggage of invokeAndBlock + * @return A Media element + * @throws IOException If blocking=true and media fails to load (e.g. can't be found). + */ + private Media createMedia(InputStream stream, String mimeType, Runnable onCompletion, boolean blocking) throws IOException { + if (stream instanceof ArrayBufferInputStream){ + ArrayBufferInputStream bufStream = (ArrayBufferInputStream)stream; + String src = bufStream.getSrc(); + if (src != null){ + return createMedia(src, mimeType.indexOf("video")!=-1, mimeType, onCompletion, blocking); + } + + Blob blob = bufStream.getBlob(); + if (mimeType != null && !Objects.equals(blob.getType(), mimeType)) { + blob = BlobUtil.toType(blob, mimeType); + } + URLBuilderFactory factory = (URLBuilderFactory)window; + URLBuilder urlBuilder = factory.getURL(); + + String objUrl = urlBuilder.createObjectURL(blob); + return createMedia(objUrl, mimeType.indexOf("video")!=-1, mimeType, onCompletion, blocking); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(stream, baos); + Blob blob = BlobUtil.createBlob(baos.toByteArray(), mimeType); + URLBuilderFactory factory = (URLBuilderFactory)window; + URLBuilder urlBuilder = factory.getURL(); + String objUrl = urlBuilder.createObjectURL(blob); + return createMedia(objUrl, mimeType.indexOf("video")!=-1, mimeType, onCompletion, blocking); + } + } + + @Override + public boolean isGalleryTypeSupported(int type) { + if (super.isGalleryTypeSupported(type)) { + return true; + } + + switch (type) { + case Display.GALLERY_ALL_MULTI: + case Display.GALLERY_IMAGE_MULTI: + case Display.GALLERY_VIDEO_MULTI: + case -9999: + case -9998: + return true; + } + return false; + } + + + + @Override + public void openGallery(ActionListener response, int type) { + String accept = null; + boolean multiple = false; + switch (type) { + case -9998: + case Display.GALLERY_ALL_MULTI: + case Display.GALLERY_IMAGE_MULTI: + case Display.GALLERY_VIDEO_MULTI: + multiple = true; + break; + } + if (type == -9998) { + type = -9999; + } + switch (type) { + case Display.GALLERY_IMAGE: + case Display.GALLERY_IMAGE_MULTI: + accept = "image/*"; + break; + case Display.GALLERY_VIDEO: + case Display.GALLERY_VIDEO_MULTI: + accept = "video/*"; + break; + case -9999: + accept = Display.getInstance().getProperty("javascript.openGallery.accept", null); + break; + } + com.codename1.teavm.ext.usermedia.FileChooser chooser = new com.codename1.teavm.ext.usermedia.FileChooser(response, accept, null); + chooser.setMultiple(multiple); + if (accept != null && accept.startsWith("image/")) { + chooser.setFixImageOrientation(true); + } + chooser.showDialog(); + } + + @Override + public void captureVideo(ActionListener response) { + new com.codename1.teavm.ext.usermedia.FileChooser(response, "video/*", "camcorder").showDialog(); + } + + @Override + public void captureVideo(VideoCaptureConstraints constraints, final ActionListener response) { + if (constraints == null || constraints.isNullConstraint() || isIOS()) { + // There are no constraints here... just use the default video capture. + + // On iOS we can't really use capture constraints yet anyways + // https://github.com/collab-project/videojs-record/issues/181 + // https://github.com/collab-project/videojs-record/issues/332 + + captureVideo(response); + return; + } + + VideoJS.Options opts = VideoJS.newOptions(); + VideoJS.RecordOptions recordOptions = VideoJS.newRecordOptions(); + opts.setPlugins(VideoJS.newPlugins(recordOptions)); + VideoJS.MediaStreamConstraints videoConstraints = VideoJS.newMediaStreamConstraints(); + recordOptions.setVideo(videoConstraints); + if (constraints.getHeight() != 0) { + videoConstraints.setHeight(constraints.getHeight()); + } + if (constraints.getWidth() != 0) { + videoConstraints.setWidth(constraints.getWidth()); + } + if (constraints.getMaxLength() != 0) { + recordOptions.setMaxLength(constraints.getMaxLength()); + } + try { + final VideoJS videoJS = new VideoJS(null, opts); + videoJS.addListener(new VideoJS.VideoListener() { + @Override + public void onDeviceError(String errorCode) { + videoJS.destroy(); + Log.e(new IOException("Device error code: "+errorCode)); + response.actionPerformed(new ActionEvent(null)); + } + + @Override + public void onError(String message) { + videoJS.destroy(); + Log.e(new IOException(message)); + response.actionPerformed(new ActionEvent(null)); + } + + @Override + public void onStartRecord() { + + } + + @Override + public void onFinishRecord(Blob recordedData) { + videoJS.destroy(); + response.actionPerformed(new ActionEvent(HTML5Implementation.createTempFile(recordedData))); + } + }); + } catch (IOException ioe) { + Log.e(ioe); + Log.p("VideoJS is not loaded, using default captureVideo behaviour. Add the javascript.includeVideoJS build hint in order to use capture video with video constraints support."); + captureVideo(response); + } + } + + + + @Override + public void captureAudio(ActionListener response) { + //new com.codename1.teavm.ext.usermedia.FileChooser(response, "audio/*", "capture").showDialog(); + super.captureAudio(response); + } + + @Override + public String[] getAvailableRecordingMimeTypes() { + return new String[]{"audio/wav"}; + } + + @Override + public Media createMediaRecorder(String path, String mimeType) throws IOException { + return createMediaRecorder(new MediaRecorderBuilder().path(path).mimeType(mimeType)); + } + + @Override + public Media createMediaRecorder(MediaRecorderBuilder builder) throws IOException { + return new HTML5MediaRecorder(builder); + } + + + + + + + @Override + public void capturePhoto(final ActionListener response) { + String defaultGetUserMedia = CN.isDesktop() ? "true" : "false"; + if (!PhotoCapture.isSupported() || Display.getInstance().getProperty("javascript.useGetUserMedia", defaultGetUserMedia).equals("false")) { + com.codename1.teavm.ext.usermedia.FileChooser chooser = new com.codename1.teavm.ext.usermedia.FileChooser(response, "image/*", "camera"); + // On iOS we need to fix image orientation + // https://github.com/codenameone/CodenameOne/issues/2694 + chooser.setFixImageOrientation(true); + chooser.showDialog(); + return; + } + + PhotoCapture capture = new PhotoCapture(); + HTMLCanvasElement photo = capture.showDialog(); + if (photo == null) { + Display.getInstance().callSerially(new Runnable(){ + + @Override + public void run() { + response.actionPerformed(new ActionEvent(null)); + } + + }); + return; + } + FileSystemStorage fs = FileSystemStorage.getInstance(); + String imagePath = generateUniqueImagePath(fs, "photo", "png"); + try { + + OutputStream fos = fs.openOutputStream(imagePath); + Blob blob = BlobUtil.canvasToBlob(photo, "image/png", 100); + + InputStream blobInput = BlobUtil.openInputStream(blob); + Util.copy(blobInput, fos); + Util.cleanup(blobInput); + Util.cleanup(fos); + + } catch (IOException ex){ + //Log.e(ex); + consoleLog("Error in capturePhoto"); + consoleLog(ex.getMessage()); + imagePath = null; + } + final String path = imagePath; + Display.getInstance().callSerially(new Runnable(){ + + @Override + public void run() { + response.actionPerformed(new ActionEvent(path)); + } + + }); + + } + + + public void capturePhoto_old(final ActionListener response) { + WindowExt win = (WindowExt)window; + CN1Native cn1 = win.getCn1(); + + FileSystemStorage fs = FileSystemStorage.getInstance(); + + + + cn1.capturePhoto(new JSOImplementations.CapturePhotoCallback() { + + @Override + public void callback(final HTMLCanvasElement canvas) { + + new Thread(){ + public void run(){ + FileSystemStorage fs = FileSystemStorage.getInstance(); + String imagePath = generateUniqueImagePath(fs, "photo", "png"); + try { + + OutputStream fos = fs.openOutputStream(imagePath); + Blob blob = BlobUtil.canvasToBlob(canvas, "image/png", 100); + + InputStream blobInput = BlobUtil.openInputStream(blob); + Util.copy(blobInput, fos); + Util.cleanup(blobInput); + Util.cleanup(fos); + + } catch (IOException ex){ + Log.e(ex); + imagePath = null; + } + final String path = imagePath; + Display.getInstance().callSerially(new Runnable(){ + + @Override + public void run() { + response.actionPerformed(new ActionEvent(path)); + } + + }); + } + }.start(); + } + }, cameraWidth, cameraHeight); + } + + + private String generateUniqueImagePath(FileSystemStorage fs, String baseName, String ext){ + if (!fs.exists(photosPath)) { + fs.mkdir(photosPath); + } + String prefix = baseName; + String suffix = ""; + char sep = fs.getFileSystemSeparator(); + String out = photosPath+sep+prefix+suffix+"."+ext; + while (fs.exists(out)){ + if ("".equals(suffix)){ + suffix="-1"; + } else { + suffix = "-"+(Integer.parseInt(suffix.substring(1))+1); + } + out = photosPath+sep+prefix+suffix+"."+ext; + } + return out; + } + + @JSBody(params={"el", "selector"}, script="return jQuery(el).is(selector)") + private native static boolean jQuery_is_(JSObject el, String selector); + + @JSBody(params={"y"}, script="jQuery(window).scrollTop(y)") + private native static void scrollToY(int y); + + @JSBody(params={}, script="return jQuery(window).scrollTop()") + private native static int getScrollY_(); + + @JSBody(params={}, script="jQuery(window).scroll()") + private native static void scroll_(); + + private boolean isEditing=false; + + /** + * Text field that is used for editing. + * Due to a bug in Chrome that kills performance if you add a text field, focus it, + * and remove it, we create one field ONE TIME, then just show it and hide it. + * DO NOT REMOVE THIS FIELD FROM THE DOM or it will kill canvas performance in + * Chrome for some unexplicable reason. Related to https://github.com/shannah/cn1-teavm-port/issues/31 + */ + private HTMLInputElement textField, textArea; + private boolean isEditingSingleLine; + private boolean doneEventFired; + private boolean tabNext, tabPrev; + + private int vkbHeight; + + // To alleviate race conditions during editing we keep track of + // the timestamps when each field is updated (i.e. the codenameone + // text field vs the native text field + //private long lastCN1InputTime, lastHTMLInputTime; + + + private void safeSleep(final int millis) { + Display.getInstance().invokeAndBlock(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(millis); + } catch (InterruptedException ex) { + //Log.e(ex); + } + } + + }); + } + + private void focusInputElement() { + if (isEditing && currentInputField != null && !jQuery_is_(currentInputField, ":focus")) { + currentInputField.focus(); + } + } + + boolean isNativeInputFieldFocused() { + return (isEditing && currentInputField != null && jQuery_is_(currentInputField, ":focus")); + } + + @Override + public boolean isEditingText(Component c) { + NativeOverlay overlay = (NativeOverlay)c.getNativeOverlay(); + if (overlay != null && jQuery_is_(overlay.el, ":focus")) { + return true; + + } + return currentEditingField == c && isEditing; + //return super.isEditingText(c); //To change body of generated methods, choose Tools | Templates. + } + + + + + + @Override + public boolean isNativeEditorVisible(Component c) { + NativeOverlay overlay = (NativeOverlay)c.getNativeOverlay(); + if (overlay != null && jQuery_is_(overlay.el, ":focus")) { + return true; + } + return currentEditingField == c && isEditing; + } + + private int lastEditorTop,lastEditorLeft,lastEditorWidth,lastEditorHeight; + + + /** + * Scales a coordinate from CN1 space to DOM one space. + * @param x + * @return + */ + public static int scaleCoord(int x) { + return (int)(x / getDevicePixelRatio()); + } + + public static double scaleCoord(double x) { + return x / getDevicePixelRatio(); + } + + + + /** + * Scales a coordinate from DOM space to the CN1 space. + * @param x + * @return + */ + public static int unscaleCoord(int x) { + return (int)(x * getDevicePixelRatio()); + } + + private void resizeNativeEditor() { + if (isEditing && currentInputField != null && currentEditingField != null) { + HTMLInputElement inputEl = currentInputField; + TextArea ta = currentEditingField; + Component cmp = ta; + Style taStyle = ta.getStyle(); + + int paddingTop = taStyle.getPadding(Component.TOP);; + int paddingLeft = taStyle.getPadding(ta.isRTL(), Component.LEFT); + int paddingRight = taStyle.getPadding(ta.isRTL(), Component.RIGHT); + int paddingBottom = taStyle.getPadding(Component.BOTTOM); + + int newTop = scaleCoord(cmp.getAbsoluteY()+cmp.getScrollY()); + int newLeft = scaleCoord(cmp.getAbsoluteX()+cmp.getScrollX()); + int newWidth = scaleCoord(cmp.getWidth()-paddingLeft-paddingRight); + int newHeight = scaleCoord(cmp.getHeight()-paddingTop-paddingBottom); + + if (lastEditorTop != newTop || lastEditorLeft != newLeft || lastEditorWidth != newWidth || lastEditorHeight != newHeight) { + //String msg = "Resizing editor from "+lastEditorTop+","+lastEditorLeft+","+lastEditorWidth+","+lastEditorHeight+" to "+newTop+","+newLeft+","+newWidth+","+newHeight; + //consoleLog(msg); + inputEl.getStyle().setProperty("padding-top", scaleCoord((double)paddingTop)+"px"); + inputEl.getStyle().setProperty("padding-left", scaleCoord((double)paddingLeft)+"px"); + inputEl.getStyle().setProperty("padding-bottom", scaleCoord((double)paddingBottom)+"px"); + inputEl.getStyle().setProperty("padding-right", scaleCoord((double)paddingRight)+"px"); + inputEl.getStyle().setProperty("top", newTop+"px"); + inputEl.getStyle().setProperty("left", newLeft+"px"); + inputEl.getStyle().setProperty("width", newWidth+"px"); + inputEl.getStyle().setProperty("height", newHeight+"px"); + inputEl.getStyle().setProperty("border", "none"); + inputEl.getStyle().setProperty("margin", "0"); + + lastEditorTop = newTop; + lastEditorLeft = newLeft; + lastEditorWidth = newWidth; + lastEditorHeight = newHeight; + } + } + } + + public static Runnable wrapOnEdt(final Runnable r) { + return new Runnable() { + + @Override + public void run() { + callSerially(r); + } + + }; + } + + public Runnable wrapOnNativeQueue(final Runnable r) { + return new Runnable() { + + @Override + public void run() { + nativeCallSerially(r); + + } + + }; + } + + /** + * Flag to indicate whether we use the preemptive native text field + * approach for text editing. With this approach we create the native + * text field and focus is as soon as a native touch event occurs over a text + * field - ON THE NATIVE THREAD - rather than waiting for the editString() method + * which is triggered too late, and thus prevented (by iOS) from focusing any + * text fields programmatically. This approach is a hack for iOS only. + * @return + */ + private static boolean usePreemptiveNativeTextFieldApproach() { + return isIOS(); + } + + // For iOS it will only show the "next" button if there is a tabbable + // native text field that can be focused next. + // This will be used to place such a field as a dummy + private HTMLInputElement dummyNextTextField, dummyPrevTextField; + + // On iOS, we can only focus a text field in an event that was triggered + // by the user... this can't happen on the EDT so by the time a tapped text field + // event on the EDT is processed, it's too late to do any focusing. + // So we create a native text field preemptively and focus it - this will + // be used and resized appropriately by editString() on the EDT + private HTMLInputElement preemptiveFocusTextField; + + @JSBody(params={"el"}, script="jQuery(el).on('touchstart.preemptiveFocus', " + + " function() { this.focus();}" + + "); " + + "jQuery(el).trigger('touchstart'); " + + "jQuery(el).off('.preemptiveFocus')") + native static void triggerFocusIOS(JSObject el); + + private void triggerFocusIOSTextField(final Component target, final Component cmp, final Component nextFocus) { + + } + + + + /** + * On iOS we can only trigger a text field to focus programmatically inside a limited set of event + * handlers. We can't initiate this from the EDT because it is too far removed from the original + * touch event that triggered the "TextArea.startEditing()" call. To work around this, + * we intercept all touchend events, and see if their (x,y) coordinates are over a text area. + * + * We then immediately create a native text field and focus it (for the keyboard). Then when the editString() + * method is called later from the EDT, it looks for this text field and works with that, rather than trying + * to create a new one at that time. + * + * NOTE: We can't use any synchronous stuff for this because it runs directly on Javascript native. This will + * cause warnings during compilation because there are many java classes that *could* use synchronous stuff + * but we just have to be sure that they don't. E.g. TeaVM may think that any use of java.util.List is synchronous + * because of the Vector class, but we just have to ensure that there isn't actually any synchronized, locking, etc.. + * going on. + * @param cmp + */ + // This is run on the native thread. + private void triggerFocusIOSCmp(final Component target) { + final Component cmp; + Component nextFocus = (Component)target.getClientProperty("$$focus"); + cmp = nextFocus == null ? target : nextFocus; + + if (cmp instanceof TextArea && cmp.isEnabled() && cmp.isEditable() && cmp.getComponentForm() != null) { + TextArea ta = (TextArea)cmp; + final Form.TabIterator tabber = cmp.getComponentForm().getTabIterator(cmp); + if (!ta.isSingleLineTextArea()){ + preemptiveFocusTextField = (HTMLInputElement)window.getDocument().createElement("textarea"); + + + } else { + preemptiveFocusTextField = (HTMLInputElement)window.getDocument().createElement("input"); + preemptiveFocusTextField.setType("text"); + + } + preemptiveFocusTextField.setAttribute("class", "cn1-edit-string preemptive"); + preemptiveFocusTextField.setTabIndex(2); + + window.getDocument().getBody().appendChild(preemptiveFocusTextField); + + if (dummyNextTextField != null) { + window.getDocument().getBody().removeChild(dummyNextTextField); + dummyNextTextField = null; + } + if (tabber.hasNext()) { + Component next = tabber.getNext(); + dummyNextTextField = (HTMLInputElement)window.getDocument().createElement("input"); + dummyNextTextField.setAttribute("class", "cn1-edit-string dummy-next"); + dummyNextTextField.getStyle().setProperty("pointer-events", "none"); + dummyNextTextField.getStyle().setProperty("opacity", "0"); + CSSStyleDeclaration s = dummyNextTextField.getStyle(); + + s.setProperty("top", scaleCoord(next.getAbsoluteY()+next.getScrollY())+"px"); + s.setProperty("left", scaleCoord(next.getAbsoluteX()+next.getScrollX())+"px"); + s.setProperty("width", scaleCoord(next.getWidth())+"px"); + s.setProperty("height", scaleCoord(next.getHeight())+"px"); + s.setProperty("border", "none"); + s.setProperty("margin", "0"); + s.setProperty("outline", "none"); // for chrome + dummyNextTextField.setType("text"); + + dummyNextTextField.setTabIndex(3); + dummyNextTextField.addEventListener("focus", new EventListener() { + + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + nextEditPending = false; + Form f = _getCurrent(); + if (f != null) { + final Component next = tabber.getNext(); + nextEditPending = next instanceof TextArea; + if (next != null) { + triggerFocusIOSCmp(next); + + callSerially(new Runnable() { + + @Override + public void run() { + + next.requestFocus(); + next.startEditingAsync(); + } + + }); + } + } + if (!nextEditPending) { + outputCanvas.focus(); + window.getDocument().getBody().removeChild(dummyNextTextField); + dummyNextTextField = null; + } + } + + }); + + preemptiveFocusTextField.addEventListener("focus", new EventListener() { + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + if (isIOS()) { + new Thread(new Runnable() { + public void run() { + Display.getInstance().fireVirtualKeyboardEvent(true); + } + }).start(); + + } + } + }); + + preemptiveFocusTextField.addEventListener("blur", new EventListener() { + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + if (isIOS()) { + new Thread(new Runnable() { + public void run() { + Display.getInstance().fireVirtualKeyboardEvent(false); + } + }).start(); + + } + if (dummyNextTextField != null) { + Form f = _getCurrent(); + if (f != null) { + final Component next = tabber.getNext(); + if (next != null) { + CSSStyleDeclaration s = dummyNextTextField.getStyle(); + + s.setProperty("top", scaleCoord(next.getAbsoluteY()+next.getScrollY())+"px"); + s.setProperty("left", scaleCoord(next.getAbsoluteX()+next.getScrollX())+"px"); + s.setProperty("width", scaleCoord(next.getWidth())+"px"); + s.setProperty("height", scaleCoord(next.getHeight())+"px"); + s.setProperty("border", "none"); + s.setProperty("margin", "0"); + s.setProperty("outline", "none"); // for chrome + _logBounds(dummyNextTextField); + + } + + } + } + } + + }); + window.getDocument().getBody().appendChild(dummyNextTextField); + } + + //----- prev start + + if (dummyPrevTextField != null) { + window.getDocument().getBody().removeChild(dummyPrevTextField); + dummyPrevTextField = null; + } + if (tabber.hasPrevious()) { + Component prev = tabber.getPrevious(); + dummyPrevTextField = (HTMLInputElement)window.getDocument().createElement("input"); + dummyPrevTextField.setAttribute("class", "cn1-edit-string dummy-prev"); + dummyPrevTextField.getStyle().setProperty("pointer-events", "none"); + dummyPrevTextField.getStyle().setProperty("opacity", "0"); + CSSStyleDeclaration s = dummyPrevTextField.getStyle(); + + s.setProperty("top", scaleCoord(prev.getAbsoluteY()+prev.getScrollY())+"px"); + s.setProperty("left", scaleCoord(prev.getAbsoluteX()+prev.getScrollX())+"px"); + s.setProperty("width", scaleCoord(prev.getWidth())+"px"); + s.setProperty("height", scaleCoord(prev.getHeight())+"px"); + s.setProperty("border", "none"); + s.setProperty("margin", "0"); + s.setProperty("outline", "none"); // for chrome + dummyPrevTextField.setType("text"); + + dummyPrevTextField.setTabIndex(1); + dummyPrevTextField.addEventListener("focus", new EventListener() { + + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + prevEditPending = false; + Form f = _getCurrent(); + if (f != null) { + final Component prev = tabber.getPrevious(); + prevEditPending = prev instanceof TextArea; + if (prev != null) { + triggerFocusIOSCmp(prev); + + callSerially(new Runnable() { + + @Override + public void run() { + + prev.requestFocus(); + prev.startEditingAsync(); + } + + }); + } + } + if (!prevEditPending) { + outputCanvas.focus(); + window.getDocument().getBody().removeChild(dummyPrevTextField); + dummyPrevTextField = null; + } + } + + }); + preemptiveFocusTextField.addEventListener("blur", new EventListener() { + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + if (dummyPrevTextField != null) { + Form f = _getCurrent(); + if (f != null) { + final Component prev = tabber.getPrevious(); + if (prev != null) { + CSSStyleDeclaration s = dummyPrevTextField.getStyle(); + + s.setProperty("top", scaleCoord(prev.getAbsoluteY()+prev.getScrollY())+"px"); + s.setProperty("left", scaleCoord(prev.getAbsoluteX()+prev.getScrollX())+"px"); + s.setProperty("width", scaleCoord(prev.getWidth())+"px"); + s.setProperty("height", scaleCoord(prev.getHeight())+"px"); + s.setProperty("border", "none"); + s.setProperty("margin", "0"); + s.setProperty("outline", "none"); // for chrome + + } + + } + } + } + + }); + window.getDocument().getBody().appendChild(dummyPrevTextField); + } + + //----- prev end + + + CSSStyleDeclaration st = preemptiveFocusTextField.getStyle(); + + st.setProperty("top", scaleCoord(cmp.getAbsoluteY()+cmp.getScrollY())+"px"); + st.setProperty("left", scaleCoord(cmp.getAbsoluteX()+cmp.getScrollX())+"px"); + st.setProperty("width", scaleCoord(cmp.getWidth())+"px"); + st.setProperty("height", scaleCoord(cmp.getHeight())+"px"); + _logBounds(preemptiveFocusTextField); + triggerFocusIOS(preemptiveFocusTextField); + } + } + + @JSBody(params={"el"}, script="console.log(el.getBoundingClientRect());") + native static void _logBounds(HTMLInputElement el); + + // This is run on the native thread + private void createAndFocusTextFieldPreemptively(int x, int y) { + if (usePreemptiveNativeTextFieldApproach()) { + Form f = _getCurrent(); + if (f != null) { + triggerFocusIOSCmp(f.getComponentAt(x, y)); + } + + } + } + + @Override + public void stopTextEditing() { + if (isEditing){ + if (currentEditingField != null) { + pendingTextChanges = currentEditingField.getText(); + } + if (!editingStartingUp) { + finishTextEditing(); + } else { + editingStartingUp = false; + } + } + } + + private NativePicker activePicker; + + @Override + public Object showNativePicker(int type, Component source, Object currentValue, Object data) { + if (!isNativePickerTypeSupported(type)) { + return super.showNativePicker(type, source, data, data); + } + if (activePicker != null) { + throw new IllegalStateException("Attempt to show native picker while another picker is still active."); + } + activePicker = NativePicker.createNativePicker(type, source, currentValue, data); + try { + return activePicker.show(); + } finally { + activePicker = null; + } + + } + + @Override + public boolean isNativePickerTypeSupported(int pickerType) { + return NativePicker.isNativePickerTypeSupported(pickerType); + } + + + + + private HTMLInputElement inputEl; + private String text; + private DataChangedListener dataChangedListener; + private Runnable editingCompleteCallback; + private boolean nextEditPending, prevEditPending; + + + @Override + public void editString(final Component cmp, int maxSize, int constraint, final String origText, int initiatingKeycode) { + if (cmp.getNativeOverlay() != null) { + // If a native overlay exists then just use that native overlay + NativeOverlay overlayEl = (NativeOverlay)cmp.getNativeOverlay(); + overlayEl.el.focus(); + return; + } + if (usePreemptiveNativeTextFieldApproach() && preemptiveFocusTextField == null) { + // On iOS we depend on the text field to have been set up in the touchend native + // event. If it isn't set up, then we won't proceed. + return; + } + if (isEditing) { + return; + } + text = origText; + isEditing=true; + + // This Hack is specifically to work around an issue on iOS Safari + // where the "touch" event circumvents the editing process, causing + // the user to tap 3 times to edit a text field. With this hack + // we have it down to 2 taps. + editingStartingUp = true; + + Window.setTimeout(new TimerHandler() { + + @Override + public void onTimer() { + editingStartingUp = false; + } + + }, 3000); + inputEl = null; + + final Runnable cleanup = new Runnable() { + public void run() { + if (inputEl != null) { + // Hide the text field. Don't get crafty and try to + // remove it due to bug in Chrome + // https://github.com/shannah/cn1-teavm-port/issues/31 + inputEl.getStyle().setProperty("display", "none"); + if ("password".equalsIgnoreCase(inputEl.getAttribute("type"))) { + inputEl.setAttribute("type", "text"); + } + //outputCanvas.focus(); + inputEl = null; + } + isEditing = false; + Display.getInstance().onEditingComplete(cmp, text); + if (doneEventFired && cmp instanceof TextArea) { + ((TextArea)cmp).fireDoneEvent(); + } + if (tabNext) { + Form f = Display.getInstance().getCurrent(); + if (f != null) { + Component focused = f.getNextComponent(cmp); + + if (focused != null) { + if (!(focused instanceof TextArea)) { + final Component fFocused = focused; + UITimer.timer(300, false, new Runnable() { + public void run() { + // This delay is necessary on Android + // to give it time to close the keyboard + fFocused.requestFocus(); + fFocused.startEditingAsync(); + outputCanvas.focus(); + + } + }); + + } else { + focused.requestFocus(); + focused.startEditingAsync(); + } + + } + + } + } else if (tabPrev) { + Form f = Display.getInstance().getCurrent(); + if (f != null) { + + Component focused = f.getPreviousComponent(cmp); + + if (focused != null) { + if (!(focused instanceof TextArea)) { + final Component fFocused = focused; + UITimer.timer(300, false, new Runnable() { + public void run() { + // This delay is necessary on Android + // to give it time to close the keyboard + fFocused.requestFocus(); + fFocused.startEditingAsync(); + outputCanvas.focus(); + + } + }); + } else { + focused.requestFocus(); + focused.startEditingAsync(); + } + } + + } + } + doneEventFired = false; + tabNext = false; + tabPrev = false; + pendingTextChanges = null; + } + }; + + try { + + if (cmp == null) { + + throw new IllegalArgumentException("component is null"); + } + + if (!(cmp instanceof TextArea)) { + throw new IllegalArgumentException("component must be instance of TextArea"); + } + + final TextArea ta = (TextArea)cmp; + + inputEl = ta.isSingleLineTextArea() ? textField : textArea; + isEditingSingleLine = ta.isSingleLineTextArea(); + final boolean hasDoneListener = ta.getDoneListener() != null; + if (inputEl == null || preemptiveFocusTextField != null) { + if (preemptiveFocusTextField != null && inputEl != null) { + window.getDocument().getBody().removeChild(inputEl); + } + if (!ta.isSingleLineTextArea()){ + if (preemptiveFocusTextField != null) { + inputEl = textArea = preemptiveFocusTextField; + } else { + inputEl = textArea = (HTMLInputElement)window.getDocument().createElement("textarea"); + } + + + } else { + if (preemptiveFocusTextField != null) { + inputEl = textField = preemptiveFocusTextField; + } else { + inputEl = textField = (HTMLInputElement)window.getDocument().createElement("input"); + inputEl.setType("text"); + } + + } + + + inputEl.setAttribute("class", "cn1-edit-string"); + + inputEl.addEventListener("keydown", new EventListener() { + + @Override + public void handleEvent(final Event evt) { + final KeyEvent kevt = (KeyEvent)evt; + switch (((KeyEvent)evt).getKeyCode()) { + case 9 : + case 11 : + case 10 : + case 13 : + if (isEditingSingleLine || hasDoneListener || kevt.getKeyCode() == 9 || kevt.getKeyCode() == 11) { + evt.preventDefault(); + evt.stopPropagation(); + nativeCallSerially(new Runnable() { + public void run() { + lastCharCode = 0; + } + }); + + } + + break; + } + callSerially(new Runnable() { + public void run() { + + switch (kevt.getKeyCode()) { + case 9 : // tab + case 11 : // vertical tab + case 10 : // lf + case 13 : // cr + { + if (!(isEditingSingleLine || hasDoneListener) && kevt.getKeyCode() != 9 && kevt.getKeyCode() != 11) { + // We don't do any special handling for multiline text fields. + return; + } + doneEventFired = true; + boolean isNextButton = false; + if (isPhoneOrTablet_()) { + if (currentEditingField != null) { + Form f = currentEditingField.getComponentForm(); + if (f != null) { + Component next = f.getNextComponent(currentEditingField); + if (next != null) { + isNextButton = true; + } + } + } + } + if (kevt.getKeyCode() == 9 || kevt.getKeyCode() == 11 || isNextButton ) { + tabNext = !kevt.isShiftKey(); + tabPrev = !tabNext; + } + if (currentEditingField != null) { + pendingTextChanges = currentEditingField.getText(); + } + finishTextEditing(); + } + } + + } + }); + } + + }); + + if (preemptiveFocusTextField == null) { + window.getDocument().getBody().appendChild(inputEl); + } else { + preemptiveFocusTextField = null; + } + } + currentInputField = inputEl; + currentEditingField = ta; + // Show the text field... + inputEl.getStyle().setProperty("display", "block"); + + inputEl.setAttribute("maxlength", maxSize+""); + inputEl.getStyle().setProperty("font", ((NativeFont)cmp.getStyle().getFont().getNativeFont()).getScaledCSS()); + inputEl.getStyle().setProperty("color", HTML5Graphics.color(cmp.getStyle().getFgColor())); + + final Style taStyle = ta.getStyle(); + Font font = taStyle.getFont(); + //int txty = ta.getAbsoluteY(); + //int txtx = ta.getAbsoluteX(); + int paddingTop = taStyle.getPadding(Component.TOP);; + int paddingLeft = taStyle.getPadding(ta.isRTL(), Component.LEFT); + int paddingRight = taStyle.getPadding(ta.isRTL(), Component.RIGHT); + int paddingBottom = taStyle.getPadding(Component.BOTTOM); + + if (ta.isSingleLineTextArea()) { + + switch (ta.getVerticalAlignment()) { + case Component.BOTTOM: + paddingTop = ta.getHeight() - taStyle.getPadding(false, Component.BOTTOM) - font.getHeight(); + break; + case Component.CENTER: + paddingTop = ta.getHeight() / 2 - font.getHeight() / 2; + break; + default: + paddingTop = taStyle.getPadding(false, Component.TOP); + break; + } + } else { + paddingTop = taStyle.getPadding(false, Component.TOP); + } + + inputEl.getStyle().setProperty("padding-top", scaleCoord((double)paddingTop)+"px"); + inputEl.getStyle().setProperty("padding-left", scaleCoord((double)paddingLeft)+"px"); + inputEl.getStyle().setProperty("padding-bottom", scaleCoord((double)paddingBottom)+"px"); + inputEl.getStyle().setProperty("padding-right", scaleCoord((double)paddingRight)+"px"); + inputEl.getStyle().setProperty("top", scaleCoord((double)(cmp.getAbsoluteY()+cmp.getScrollY()))+"px"); + inputEl.getStyle().setProperty("left", scaleCoord((double)(cmp.getAbsoluteX()+cmp.getScrollX()))+"px"); + inputEl.getStyle().setProperty("width", scaleCoord((double)(cmp.getWidth()-paddingLeft-paddingRight))+"px"); + inputEl.getStyle().setProperty("height", scaleCoord((double)(cmp.getHeight()-paddingTop-paddingBottom))+"px"); + inputEl.getStyle().setProperty("border", "none"); + inputEl.getStyle().setProperty("margin", "0"); + inputEl.getStyle().setProperty("outline", "none"); // for chrome + + int cnst = ta.getConstraint(); + String inputType = "text"; + if (ta.isSingleLineTextArea()) { + + switch (cnst) { + case TextArea.PASSWORD: + inputType = "password"; + break; + case TextArea.EMAILADDR: + inputType = "email"; + break; + case TextArea.NUMERIC: + inputType = "number"; + break; + case TextArea.PHONENUMBER: + inputType = "tel"; + break; + case TextArea.URL: + inputType = "url"; + break; + + } + inputEl.setAttribute("type", inputType); + + + } + + + + boolean valid = + (initiatingKeycode > 47 && initiatingKeycode < 58) || // number keys + initiatingKeycode == 32 || initiatingKeycode == 13 || // spacebar & return key(s) (if you want to allow carriage returns) + (initiatingKeycode > 64 && initiatingKeycode < 91) || // letter keys + (initiatingKeycode > 95 && initiatingKeycode < 112) || // numpad keys + (initiatingKeycode > 185 && initiatingKeycode < 193) || // ;=,-./` (in order) + (initiatingKeycode > 218 && initiatingKeycode < 223); // [\]' (in order) + + switch (initiatingKeycode) { + case 8 : { // backspace + if (valid && text.length() > 0) { + text = text + (char)initiatingKeycode; + } + break; + } + + case 10 : + case 13 : { // newline + if (ta.isSingleLineTextArea()) { + Display.getInstance().onEditingComplete(cmp, text); + isEditing = false; + return; + } else if (valid) { + text = text + (char) initiatingKeycode; + } + break; + } + + case 0 : // Null char + break; + default : { + if (valid) { + text = text + (char) initiatingKeycode; + } + } + + + } + + + + inputEl.setValue(text); + if ("text".equals(inputType)) { + ((TextElement)inputEl).setSelectionRange(text.length(), text.length()); + } + + + Form currentForm = Display.getInstance().getCurrent(); + if (currentForm != null) { + if (currentForm.getFocused() != ta) { + ta.requestFocus(); + } + } + + final FocusListener focusListener = new FocusListener() { + + @Override + public void focusGained(Component cmpnt) { + + } + + @Override + public void focusLost(Component cmpnt) { + + finishTextEditing(); + } + + }; + + + + dataChangedListener = null; + + + final HTMLInputElement el = inputEl; + dataChangedListener = new DataChangedListener() { + + @Override + public void dataChanged(int i, int i1) { + String val = ta.getText(); + if (val != null && !val.equals(el.getValue())) { + //lastCN1InputTime = System.currentTimeMillis(); + el.setValue(val); + finishTextEditing(); + } + } + }; + ta.addDataChangedListener(dataChangedListener); + + + ta.addFocusListener(focusListener); + + + + ta.repaint(); + + doneEventFired = false; + tabNext = false; + tabPrev = false; + + final HTMLInputElement finalInputEl = inputEl; + // We need to resize the canvas whenever the soft keyboard is shown + + //inputEl.blur(); + + // A runnable to help re-layout the form and text field to deal with + // changes outside of our control (e.g. virtual keyboards causeing + // scrolling or resizing. + final Runnable layoutForm = new Runnable() { + @Override + public void run() { + + //safeSleep(100); + // On iOS and Android the VKB causes the browser to scroll + // down to the field that is being edited if it would be + // covered by the keyboard. + // We detect the scroll position and then add appropriate + // padding to the bottom of the form... then scroll up to the + // top again to compensate. + vkbHeight = getScrollY_(); + Form current = Display.getInstance().getCurrent(); + if (!current.isFormBottomPaddingEditingMode()) { + //We only re-layout the form if form bottom padding is enabled + return; + } + current.getContentPane().getUnselectedStyle().setPaddingUnit(new byte[] {Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS}); + current.getContentPane().getUnselectedStyle().setPadding(Component.BOTTOM, unscaleCoord(vkbHeight)); + + + Display.getInstance().callSerially(new Runnable() { + + @Override + public void run() { + Display.getInstance().getCurrent().forceRevalidate(); + finalInputEl.getStyle().setProperty("top", scaleCoord(cmp.getAbsoluteY()+cmp.getScrollY())+"px"); + finalInputEl.getStyle().setProperty("left", scaleCoord(cmp.getAbsoluteX()+cmp.getScrollX())+"px"); + //safeSleep(100); + scrollToY(0); + Display.getInstance().getCurrent().forceRevalidate(); + } + + }); + + + } + }; + + // A handler for the input into the text field to fire data change listeners etc.. + EditingInputHandler inputHandler = new EditingInputHandler(ta, inputEl, layoutForm); + + final JSFunction inputHandle = EventUtil.addEventListener(inputEl, "input", inputHandler); + + // We need to listen for resize events because some platforms (e.g. MS Surface) + // will resize the window to show the VKB. + final JSFunction resizeHandle = EventUtil.addEventListener(window, "resize", new EventListener() { + + @Override + public void handleEvent(Event evt) { + callSerially(new Runnable() { + public void run() { + safeSleep(50); + layoutForm.run(); + } + }); + } + + }); + + // On some platforms (Android and iOS) it can be very hard to + // track down the final scroll position of the page as a result + // of showing the VKB. For this reason we set an interval to + // continually check the scroll and focus every 100ms and + // adjust the layout accordingly. + final int[] intervalCounter = new int[1]; + + + + final int intervalHandle = Window.setInterval(new TimerHandler() { + @Override + public void onTimer() { + + if (!jQuery_is_(finalInputEl, ":focus")) { + finalInputEl.focus(); + } + + if (getScrollY_() > 1) { + callSerially(layoutForm); + } + + } + }, 100); + + // We only want the interval to run for a maximum of 2 seconds + // since that should be sufficient to detect any scroll changes + // as a result of the VKB being shown. Set a timer for + // 2 seconds which just clears the interval. + final boolean[] intervalCleared = new boolean[1]; + Window.setTimeout(new TimerHandler() { + public void onTimer() { + if (!intervalCleared[0]) { + intervalCleared[0] = true; + window.clearInterval(intervalHandle); + } + } + }, 2000); + + // In order to force the VKB to appear on touch devices we'll force + // the input field to focus. + // THIS DOESN' WORK ON IOS SAFARI since you can only programmatically + // focus a field as a result of a user event.... But it does work + // in every other browser/platform. + // On iOS the user has to click the field twice for now.... once + // to make it visible, and a second time to focus it. + inputEl.focus(); + + // This is where we block the EDT and wait for the editing lock to be + // released. + + + + editingCompleteCallback = new Runnable() { + public void run() { + try { + // Fix for https://github.com/shannah/cn1-teavm-port/issues/48 + // on iPad the invisible input field may retain focus and we can't + // seem to return focus to the canvas. + if (!usePreemptiveNativeTextFieldApproach() || (!nextEditPending && !prevEditPending)) { + + inputEl.blur(); + outputCanvas.focus(); + } else { + nextEditPending = false; + prevEditPending = false; + } + + // Remove all of the event listeners that we added to the input field + // and text field before blocking. + + EventUtil.removeEventListener(inputEl, "input", inputHandle); + //EventUtil.removeEventListener(inputEl, "blur", blurHandle); + //EventUtil.removeEventListener(inputEl, "focus", focusHandle); + //EventUtil.removeEventListener(inputEl, "click", clickHandle); + EventUtil.removeEventListener(window, "resize", resizeHandle); + if (!intervalCleared[0]) { + intervalCleared[0] = true; + window.clearInterval(intervalHandle); + } + + ta.removeFocusListener(focusListener); + if (dataChangedListener != null && ta instanceof TextField) { + ((TextField)ta).removeDataChangeListener(dataChangedListener); + } + + //if (lastHTMLInputTime > lastCN1InputTime) { + text = inputEl.getValue(); + //} else { + // text = ta.getText(); + //} + + + Form current = Display.getInstance().getCurrent(); + current.getContentPane().getUnselectedStyle().setPaddingUnit(new byte[] {Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS}); + current.getContentPane().getUnselectedStyle().setPadding(Component.BOTTOM, 0); + current.forceRevalidate(); + + + if (pendingTextChanges != null && !pendingTextChanges.equals(ta.getText())) { + pendingTextChanges = null; + text = ta.getText(); + } + + } finally { + cleanup.run(); + } + } + }; + /* + Display.getInstance().invokeAndBlock(new Runnable(){ + + @Override + public void run() { + while (isEditing){ + synchronized (editingLock){ + try { + editingLock.wait(1000); + } catch (InterruptedException ex) { + //Log.e(ex); + } + } + } + } + + }); + */ + } catch (Throwable t) { + cleanup.run(); + } + + + + } + + private int inputIdCounter=0; + + public static HTML5Implementation getInstance() { + return instance; + } + + @Override + public PeerComponent createNativePeer(Object nativeComponent) { + return new HTML5Peer((HTMLElement)nativeComponent); + } + + + + private static class EditingInputHandler implements EventListener { + + private final TextArea ta; + private final HTMLInputElement el; + private final int id; + private Runnable layoutForm; + + EditingInputHandler(TextArea ta, HTMLInputElement el, Runnable layoutForm) { + this.ta = ta; + this.el = el; + this.id = instance.inputIdCounter++; + this.layoutForm = layoutForm; + } + + @Override + public void handleEvent(Event evt) { + //instance.lastHTMLInputTime = System.currentTimeMillis(); + callSerially(new Runnable() { + public void run() { + if (!ta.hasFocus()) { + // As long as we're typing in the field + // The CN1 text field should have focus. + ta.requestFocus(); + } + String val = el.getValue(); + if (val != null && !val.equals(ta.getText())) { + ta.setText(el.getValue()); + } + } + }); + /* + if (getScrollY_() > 1) { + callSerially(layoutForm); + } + */ + } + + } + /** + * Flag to indicate whether we use overlay text fields on touch devices rather than + * the "old" way of creating a native overlay on demand. + * + * NOTE: Native overlays are completely disabled right now as they introduced problems. + */ + private boolean useNativeOverlaysForTextFieldsOnTouchDevices=false; + + private boolean useNativeOverlaysForTextFields() { + return useNativeOverlaysForTextFieldsOnTouchDevices && isPhoneOrTablet_(); + } + + private void finishTextEditing(){ + if (!useNativeOverlaysForTextFields()) { + if (editingCompleteCallback != null) { + Display.getInstance().callSerially(editingCompleteCallback); + editingCompleteCallback = null; + } else { + isEditing=false; + } + } + + } + + /** + * Since we are using requestAnimationFrame() we are running + * the graphics output on a native thread so we don't have regular + * locking... we need to make sure that they don't conflict. + */ + boolean graphicsLocked; + + @Override + public void flushGraphics(int x, int y, int width, int height) { + JavaScriptRenderQueueCoordinator.waitUntilFlushable(new JavaScriptRenderQueueCoordinator.FlushBarrier() { + @Override + public boolean isGraphicsLocked() { + return graphicsLocked; + } + + @Override + public void sleep(int millis) throws InterruptedException { + Thread.sleep(millis); + } + }, pendingDisplay); + + synchronized(pendingDisplay){ + /* + CanvasRenderingContext2D context = (CanvasRenderingContext2D)outputCanvas.getContext("2d"); + List ops = graphics.flush(x, y, width, height); + for (ExecutableOp op : ops){ + op.execute(context); + } + */ + JavaScriptRenderQueueCoordinator.queueFlush(new JavaScriptRenderQueueCoordinator.GraphicsLock() { + @Override + public void setGraphicsLocked(boolean locked) { + graphicsLocked = locked; + } + }, pendingDisplay, graphics.flush(x, y, width, height), x, y, width, height); + } + if (isEditing) { + resizeNativeEditor(); + } + if (activePicker != null) { + activePicker.resizeNativeElement(); + } + + + } + + @Override + public void flushGraphics() { + flushGraphics(0,0,canvas.getWidth(), canvas.getHeight()); + + } + + @Override + public void getRGB(Object nativeImage, int[] arr, int offset, int x, int y, int width, int height) { + NativeImage im = (NativeImage)nativeImage; + if (im.img != null && !im.loaded) { + im.load(); + } + final ImageData[] imData = new ImageData[1]; + JavaScriptNativeImageAdapter.readPixels(im.getImageModel(), new JavaScriptNativeImageAdapter.PixelReadTarget() { + @Override + public void readLoadedImage() { + imData[0] = renderingBackend.readLoadedImageData(im.img, x, y, width, height); + } + + @Override + public void readMutableSurface() { + imData[0] = renderingBackend.readMutableSurfaceData(im.mutableGraphics.getCanvas(), x, y, width, height); + } + }); + if (imData[0] == null) { + throw new RuntimeException("Failed to get RGB data. Image not loaded " + nativeImage); + } + + final Uint8ClampedArray dataArr = imData[0].getData(); + JavaScriptImageDataAdapter.readRgbaToArgb(new JavaScriptImageDataAdapter.PixelReader() { + @Override + public int get(int index) { + return dataArr.get(index); + } + + @Override + public int length() { + return dataArr.getLength(); + } + }, arr, offset); + + + } + + + + @Override + public LocationManager getLocationManager() { + return new HTML5LocationManager(); + } + + + + @Override + public ImageIO getImageIO() { + return new ImageIO(){ + + @Override + public void save(InputStream image, OutputStream response, String format, int width, int height, float quality) throws IOException { + Image img = Image.createImage(image).scaled(width, height); + if (width < 0) { + width = img.getWidth(); + } + if (height < 0) { + height = img.getHeight(); + } + //NativeImage nimg = (NativeImage)createImage(image); + saveImage(img, response, format, quality); + } + + private void saveImage(NativeImage nimg, OutputStream response, String format, int width, int height, float quality) throws IOException { + HTMLCanvasElement canvas = renderingBackend.createCanvas(width, height); + CanvasRenderingContext2D ctx = renderingBackend.getContext(canvas); + nimg.draw(ctx, 0, 0, width, height); + Blob blob = renderingBackend.toImageBlob(canvas, "image/"+format, quality); + InputStream blobInput = BlobUtil.openInputStream(blob); + Util.copy(blobInput, response); + Util.cleanup(blobInput); + + + } + + @Override + protected void saveImage(Image image, OutputStream response, String format, float quality) throws IOException { + + saveImage((NativeImage)image.getImage(), response, format, image.getWidth(), image.getHeight(), quality); + } + + @Override + public boolean isFormatSupported(String format) { + return ImageIO.FORMAT_JPEG.equals(format) || ImageIO.FORMAT_PNG.equals(format); + } + + }; + } + + + + @Override + public Object createImage(int[] rgb, int width, int height) { + NativeImage img = new NativeImage(); + ImageData data = (ImageData)createImageData(rgb, width, height); + JavaScriptCanvasImageBufferLifecycle.CanvasImageBuffer buffer = + JavaScriptCanvasImageBufferLifecycle.createBlankBuffer(width, height, + new JavaScriptCanvasImageBufferLifecycle.SizedCanvasFactory() { + @Override + public HTMLCanvasElement createCanvas(int canvasWidth, int canvasHeight) { + return renderingBackend.createCanvas(canvasWidth, canvasHeight); + } + }, new JavaScriptCanvasImageBufferLifecycle.GraphicsFactory() { + @Override + public HTML5Graphics createGraphics(HTMLCanvasElement canvas) { + return renderingBackend.createGraphics(HTML5Implementation.this, canvas); + } + + @Override + public void fillRect(HTML5Graphics graphics, int fillColor, int fillWidth, int fillHeight) { + } + }); + attachMutableImageSurface(img, buffer.getGraphics()); + renderingBackend.writeImageData(buffer.getCanvas(), data, width, height); + //System.out.println("Created image from rgb "+img); + + return img; + + } + + @Override + public boolean isAlphaGlobal() { + return true; + } + + @Override + public boolean isAlphaMutableImageSupported() { + return true; + } + + + + + + private Object createImageData(int[] rgb, int width, int height){ + return createImageData(rgb, 0, width, height); + } + + Object createImageData(int[] rgb, int offset, int width, int height) { + final Uint8ClampedArray arr = Uint8ClampedArray.create(width*height*4); + JavaScriptImageDataAdapter.writeArgbToRgba(rgb, offset, width, height, new JavaScriptImageDataAdapter.PixelWriter() { + @Override + public void set(int index, int value) { + arr.set(index, value); + } + }); + ImageData d = graphics.getContext().createImageData(width, height); + ((Uint8ClampedArraySetter)d.getData()).set(arr); + return d; + + } + + private int isTablet = -1; + + @Override + public boolean isTablet() { + + if (isTablet == -1) { + String overrideVal = getParameterByName("isTablet"); + if ("1".equals(overrideVal)) { + isTablet = 1; + } else if ("0".equals(overrideVal)) { + isTablet = 0; + } else { + isTablet = isPhone_() ? 0:1; + } + } + return isTablet==1; + } + + + @JSBody(params={"url", "target"}, script="window.open(url, target)") + private native static void windowOpen(String url, String target); + + + @JSBody(params={"fileName", "blob"}, script="window.cn1SaveBlobHandler = function() {if (window.navigator && window.navigator.msSaveOrOpenBlob) {\n" + + " window.navigator.msSaveOrOpenBlob(blob, fileName);\n" + + "}\n" + + "else {\n" + + " var downloadLink = document.createElement('a');" + + "downloadLink.href = URL.createObjectURL(blob);\n" + + "downloadLink.download = fileName;" + + "document.body.appendChild(downloadLink);" + + " downloadLink.click();" + + "document.body.removeChild(downloadLink);\n" + + "window.cn1SaveBlobHandler = null;" + + "}};") + private static native void registerSaveBlobHandler(String fileName, Blob blob); + + @JSBody(params={"fileName", "dataUrl"}, script="window.cn1SaveBlobHandler = function() {if (window.navigator && window.navigator.msSaveOrOpenBlob) {\n" + + " var blob = window.Base64ToBlob(dataUrl);" + + "window.navigator.msSaveOrOpenBlob(blob, fileName);\n" + + "}\n" + + "else {\n" + + " var downloadLink = document.createElement('a');" + + "downloadLink.href = dataUrl;\n" + + "downloadLink.download = fileName;" + + "document.body.appendChild(downloadLink);" + + " downloadLink.click();" + + "document.body.removeChild(downloadLink);\n" + + "window.cn1SaveBlobHandler = null;" + + "}};") + private static native void registerSaveBlobHandlerDataUrl(String fileName, String dataUrl); + + @JSBody(params={}, script="window.cn1SaveBlobHandler = null;") + private static native void deregisterSaveBlobHandler(); + + @JSBody(params={}, script="if (window.cn1SaveBlobHandler) window.cn1SaveBlobHandler();") + private static native void fireSaveBlobHandler(); + + + public boolean paintNativePeersBehind() { + return true; + } + + @JSBody(params={"js"}, script="eval(js)") + private native static void eval_(String js); + + @Override + public void execute(String url) { + + if (url.startsWith("javascript:")) { + String cmd = url.substring(url.indexOf(":")+1); + eval_(cmd); + return; + } + + String fileName = null; + boolean useBlobHandler = false; + Button nativeButton = new Button(); + if (!url.startsWith("http:") && + !url.startsWith("http:") && + !url.startsWith("mailto:") && + !url.startsWith("data:")) { + if (exists(url)) { + try { + Blob blob = openFileAsBlob(url); + char sep = getFileSystemSeparator(); + fileName = url; + if (fileName.indexOf(sep) >=0) { + fileName = fileName.substring(fileName.lastIndexOf(sep)+1); + } + //String dataUrl = blobToDataURL(blob); + //registerSaveBlobHandlerDataUrl(fileName, dataUrl); + registerSaveBlobHandler(fileName, blob); + useBlobHandler = true; + + } catch (IOException ex) { + + } + } + } + + final boolean fuseBlobHandler = useBlobHandler; + + String buttonText = null; + //String icon = null; + final String furl = url; + if (useBlobHandler) { + //popover.setContents(""); + buttonText = "Click to Download "+(fileName!=null?fileName:"File"); + nativeButton.setText(buttonText); + nativeButton.setMaterialIcon(FontImage.MATERIAL_SAVE); + } else if (url.startsWith("data:")) { + registerSaveBlobHandlerDataUrl(fileName == null ? "download":fileName, url); + //popover.setContents(""); + buttonText = "Click to Download "+(fileName!=null?fileName:"File"); + nativeButton.setText(buttonText); + nativeButton.setMaterialIcon(FontImage.MATERIAL_SAVE); + //icon = "save-file"; + } else { + //popover.setContents("Open URL in New Window"); + if (isBacksideHookAvailable()) { + addBacksideHook(new JSRunnable() { + public void run() { + window.open(furl, "New Window"); + } + }); + + } else { + final Sheet sheet = new Sheet(null, "Open Link"); + SpanLabel l = new SpanLabel("Open "+url+" in a new window?"); + Button ok = new Button("OK"); + Button cancel = new Button("Cancel"); + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + sheet.back(); + CN.execute(furl); + } + }); + cancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + sheet.back(); + } + }); + sheet.getContentPane().setLayout(BoxLayout.y()); + sheet.getContentPane().add(BorderLayout.centerEastWest(l, FlowLayout.encloseIn(ok, cancel), null)); + sheet.show(); + + } + return; + + } + if (isBacksideHookAvailable()) { + addBacksideHook(new JSRunnable() { + public void run() { + if (fuseBlobHandler) { + fireSaveBlobHandler(); + } else { + _log("Opening URL in new window"); + window.open(furl, "New Window"); + } + } + }); + + } else { + final Sheet sheet = new Sheet(null, "Download file"); + String dlName = fileName == null ? "file" : fileName; + SpanLabel l = new SpanLabel("Download "+dlName+" now?"); + Button ok = new Button("OK"); + Button cancel = new Button("Cancel"); + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + sheet.back(); + addBacksideHook(new JSRunnable() { + public void run() { + if (fuseBlobHandler) { + fireSaveBlobHandler(); + } else { + window.open(furl, "New Window"); + } + } + }); + } + }); + cancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + sheet.back(); + } + }); + sheet.getContentPane().setLayout(BoxLayout.y()); + sheet.getContentPane().add(BorderLayout.centerEastWest(l, FlowLayout.encloseIn(ok, cancel), null)); + sheet.show(); + } + + } + + + @Override + public Boolean canExecute(String url) { + return true; + } + + class HTML5Image extends com.codename1.ui.Image { + HTML5Image(NativeImage im) { + super(im); + } + } + + @Override + public boolean supportsNativeImageCache() { + return true; + } + + private void attachMutableImageSurface(final NativeImage image, HTML5Graphics graphics) { + image.mutableGraphics = graphics; + image.mutableGraphics.setMutationListener(new Runnable() { + @Override + public void run() { + JavaScriptNativeImageAdapter.invalidatePatternCache(image.getImageModel()); + } + }); + } + + + + @Override + public void downloadImageToCache(String _url, final SuccessCallback onSuccess, final FailureCallback onFail) { + if (urlProxifier != null){ + _url = urlProxifier.proxifyURL(_url); + } + final String url = _url; + final NativeImage im = new NativeImage(); + im.img = renderingBackend.createCrossOriginImageElement(url); + im.setSuppressRepaint(true); + new Thread(new Runnable() { + @Override + public void run() { + im.load(); + im.setSuppressRepaint(false); + if (im.loaded) { + final HTML5Image cn1Im = new HTML5Image(im); + + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onSuccess.onSucess(cn1Im); + } + }); + } else { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onFail.onError(this, new IOException("Failed to load image from url "+url), 500, "Failed to load image from url "+url); + } + }); + } + } + }).start(); + } + + + + @Override + public void downloadImageToStorage(String _url, final String fileName, final SuccessCallback onSuccess, final FailureCallback onFail) { + if (urlProxifier != null){ + _url = urlProxifier.proxifyURL(_url); + } + final String url = _url; + final NativeImage im = new NativeImage(); + im.img = renderingBackend.createCrossOriginImageElement(url); + im.setSuppressRepaint(true); + new Thread(new Runnable() { + @Override + public void run() { + im.load(); + im.setSuppressRepaint(false); + if (im.loaded) { + final HTML5Image cn1Im = new HTML5Image(im); + ImageIO imageIO = ImageIO.getImageIO(); + OutputStream fos = null; + try { + fos = com.codename1.io.Storage.getInstance().createOutputStream(fileName); + imageIO.save(cn1Im, fos, ImageIO.FORMAT_PNG, 1f); + } catch (final IOException ex) { + Display.getInstance().callSerially(new Runnable() { + public void run() { + onFail.onError(this, ex, 500, ex.getMessage()); + } + }); + + return; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (Exception ex){} + } + } + + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onSuccess.onSucess(cn1Im); + } + }); + } else { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onFail.onError(this, new IOException("Failed to load image from url "+url), 500, "Failed to load image from url "+url); + } + }); + } + } + }).start(); + + } + + @Override + public void downloadImageToFileSystem(String _url, final String fileName, final SuccessCallback onSuccess, final FailureCallback onFail) { + if (urlProxifier != null){ + _url = urlProxifier.proxifyURL(_url); + } + final String url = _url; + final NativeImage im = new NativeImage(); + im.img = renderingBackend.createCrossOriginImageElement(url); + im.setSuppressRepaint(true); + new Thread(new Runnable() { + @Override + public void run() { + im.load(); + im.setSuppressRepaint(false); + if (im.loaded) { + final HTML5Image cn1Im = new HTML5Image(im); + ImageIO imageIO = ImageIO.getImageIO(); + OutputStream fos = null; + try { + fos = FileSystemStorage.getInstance().openOutputStream(fileName); + imageIO.save(cn1Im, fos, ImageIO.FORMAT_PNG, 1f); + + } catch (final IOException ex) { + Display.getInstance().callSerially(new Runnable() { + public void run() { + onFail.onError(this, ex, 500, ex.getMessage()); + } + }); + + return; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (Exception ex){} + } + } + + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onSuccess.onSucess(cn1Im); + } + }); + } else { + Display.getInstance().callSerially(new Runnable() { + @Override + public void run() { + onFail.onError(this, new IOException("Failed to load image from url "+url), 500, "Failed to load image from url "+url); + } + }); + } + } + }).start(); + + } + + + + @Override + public Object createImage(String path) throws IOException { + if (exists(path) && !isDirectory(path)){ + NativeImage im = new NativeImage(); + Blob blob = openFileAsBlob(path); + im.img = renderingBackend.createBlobImageElement(blob); + im.load(); + return im; + } else { + InputStream in = this.getResourceAsStream(getClass(), path); + if (in == null) { + throw new IOException("Resource not found. " + path); + } + try { + return this.createImage(in); + } finally { + if (in != null) { + try { + in.close(); + } catch (Exception ignored) { + ; + } + } + } + } + //throw new IOException("Image could not be loaded from file "+path+" not found"); + + } + + @JSBody(params={"o"}, script="console.log(o)") + private static native void consoleLog(JSObject o); + + private static void debugLog(String str) { + if (debugFlag("debugLog")) { + consoleLog(str); + } + } + + private static void consoleLog(String str) { + consoleLog(JSString.valueOf(str)); + } + + private static void consoleLog(int val) { + consoleLog(JSNumber.valueOf(val)); + } + + @Override + public Object createImage(InputStream i) throws IOException { + int avail = i.available(); + int buffLen = 2048; + if (avail>buffLen){ + buffLen = avail; + } + byte[] buffer = new byte[buffLen]; + ArrayList bytes = new ArrayList<>(); + int num = -1; + int totalRead = 0; + while ((num=i.read(buffer)) != -1){ + bytes.add(buffer); + totalRead += num; + buffer = new byte[buffLen]; + } + + byte[] fullBuff = new byte[totalRead]; + int pos=0; + for (byte[] buff : bytes){ + System.arraycopy(buff, 0, fullBuff, pos, Math.min(buffLen, totalRead-pos)); + pos += buffLen; + } + return this.createImage(fullBuff, 0, totalRead); + + } + + + + @Override + public Object createMutableImage(int width, int height, int fillColor) { + JavaScriptCanvasImageBufferLifecycle.CanvasImageBuffer buffer = + JavaScriptCanvasImageBufferLifecycle.createMutableBuffer(width, height, fillColor, + new JavaScriptCanvasImageBufferLifecycle.SizedCanvasFactory() { + @Override + public HTMLCanvasElement createCanvas(int canvasWidth, int canvasHeight) { + return renderingBackend.createCanvas(canvasWidth, canvasHeight); + } + }, new JavaScriptCanvasImageBufferLifecycle.GraphicsFactory() { + @Override + public HTML5Graphics createGraphics(HTMLCanvasElement canvas) { + return renderingBackend.createGraphics(HTML5Implementation.this, canvas); + } + + @Override + public void fillRect(HTML5Graphics graphics, int color, int fillWidth, int fillHeight) { + graphics.setColorWithAlpha(color); + graphics.fillRect(0, 0, fillWidth, fillHeight); + } + }); + NativeImage img = new NativeImage(); + attachMutableImageSurface(img, buffer.getGraphics()); + return img; + + } + + @Override + public boolean areMutableImagesFast() { + return true; + } + + + + @Override + public Object createImage(byte[] bytes, int offset, int len) { + return createNativeImage(bytes, offset, len); + } + + @Override + public int getImageWidth(Object i) { + return ((NativeImage)i).getWidth(); + } + + @Override + public int getImageHeight(Object i) { + return ((NativeImage)i).getHeight(); + } + + @Override + public Object scale(Object nativeImage, int width, int height) { + NativeImage img = (NativeImage)nativeImage; + + NativeImage scaled = new NativeImage(); + JavaScriptCanvasImageBufferLifecycle.CanvasImageBuffer buffer = + JavaScriptCanvasImageBufferLifecycle.createBlankBuffer(width, height, + new JavaScriptCanvasImageBufferLifecycle.SizedCanvasFactory() { + @Override + public HTMLCanvasElement createCanvas(int canvasWidth, int canvasHeight) { + return renderingBackend.createCanvas(canvasWidth, canvasHeight); + } + }, new JavaScriptCanvasImageBufferLifecycle.GraphicsFactory() { + @Override + public HTML5Graphics createGraphics(HTMLCanvasElement canvas) { + return renderingBackend.createGraphics(HTML5Implementation.this, canvas); + } + + @Override + public void fillRect(HTML5Graphics graphics, int fillColor, int fillWidth, int fillHeight) { + } + }); + scaled.width = buffer.getWidth(); + scaled.height = buffer.getHeight(); + attachMutableImageSurface(scaled, buffer.getGraphics()); + if (img.img != null && !img.loaded) { + img.load(); + } + if (img.img != null && img.loaded) { + int srcW = img.img.getNaturalWidth(); + int srcH = img.img.getNaturalHeight(); + if (srcW >0 && srcH > 0) { + renderingBackend.scaleLoadedImageToCanvas(buffer.getCanvas(), img.img, srcW, srcH, width, height); + } else { + String msg = "Failed to scale image because the width or height is non-positive. "+srcW+"x"+srcH; + _log(msg); + } + } else if (img.mutableGraphics != null) { + renderingBackend.scaleMutableSurfaceToCanvas(buffer.getCanvas(), img.mutableGraphics.getCanvas(), img.getWidth(), img.getHeight(), width, height); + } + + + return scaled; + } + + public int getSoftkeyCount() { + return 0; + } + + public int[] getSoftkeyCode(int index) { + return null; + } + + + + @Override + public boolean isTouchDevice() { + return true; + } + + @Override + public boolean isMultiTouch() { + return true; + } + + @Override + public int getKeyboardType() { + return Display.KEYBOARD_TYPE_QWERTY; + } + + + + + @Override + public int getColor(Object graphics) { + return g(graphics).getColor(); + + } + + private HTML5Graphics g(Object graphics){ + return (HTML5Graphics)graphics; + } + + @Override + public void setColor(Object graphics, int RGB) { + g(graphics).setColor(RGB); + } + + @Override + public void setAlpha(Object graphics, int alpha) { + g(graphics).setAlpha(alpha); + } + + @Override + public int getAlpha(Object graphics) { + return g(graphics).getAlpha(); + } + + @Override + public void setNativeFont(Object graphics, Object font) { + if (font == null){ + font = this.getDefaultFont(); + } + g(graphics).setFont((NativeFont)font); + } + + @Override + public int getClipX(Object graphics) { + return g(graphics).getClipX(); + } + + @Override + public int getClipY(Object graphics) { + return g(graphics).getClipY(); + } + + @Override + public int getClipWidth(Object graphics) { + return g(graphics).getClipWidth(); + } + + @Override + public int getClipHeight(Object graphics) { + return g(graphics).getClipHeight(); + } + + @Override + public void setClip(Object graphics, Shape shape) { + g(graphics).setClip(shape); + } + + + + @Override + public void setClip(Object graphics, int x, int y, int width, int height) { + g(graphics).setClip(x, y, width, height); + } + + @Override + public void clipRect(Object graphics, int x, int y, int width, int height) { + g(graphics).clipRect(x, y, width, height); + } + + @Override + public void drawLine(Object graphics, int x1, int y1, int x2, int y2) { + g(graphics).drawLine(x1, y1, x2, y2); + } + + @Override + public void fillRect(Object graphics, int x, int y, int width, int height) { + g(graphics).fillRect(x, y, width, height); + } + + @Override + public void clearRect(Object graphics, int x, int y, int width, int height) { + g(graphics).clearRect(x, y, width, height); + } + + + @Override + public void fillLinearGradient(Object graphics, int startColor, int endColor, int x, int y, int width, int height, boolean horizontal) { + g(graphics).fillLinearGradient(x, y, width, height, startColor, endColor, horizontal); + } + + @Override + public void fillPolygon(Object graphics, int[] xPoints, int[] yPoints, int nPoints) { + g(graphics).fillPolygon(xPoints, yPoints, nPoints); + } + + + @Override + public void drawRect(Object graphics, int x, int y, int width, int height) { + g(graphics).drawRect(x,y,width,height); + } + + + + @Override + public void drawRoundRect(Object graphics, int x, int y, int width, int height, int arcWidth, int arcHeight) { + g(graphics).drawRoundRect(x,y,width,height,arcWidth,arcHeight); + } + + @Override + public void fillRoundRect(Object graphics, int x, int y, int width, int height, int arcWidth, int arcHeight) { + g(graphics).fillRoundRect(x,y,width,height,arcWidth,arcHeight); + } + + @Override + public void fillArc(Object graphics, int x, int y, int width, int height, int startAngle, int arcAngle) { + g(graphics).fillArc(x,y,width,height,startAngle, arcAngle); + } + + @Override + public void fillRadialGradient(Object graphics, int startColor, int endColor, int x, int y, int width, int height) { + fillRadialGradient(graphics, startColor, endColor, x, y, width, height, 0, 360); + } + @Override + public void fillRadialGradient(Object graphics, int startColor, int endColor, int x, int y, int width, int height, int startAngle, int arcAngle) { + g(graphics).fillRadialGradient(startColor, endColor, x, y, width, height, startAngle, arcAngle); + } + + + + @Override + public void drawArc(Object graphics, int x, int y, int width, int height, int startAngle, int arcAngle) { + g(graphics).drawArc(x,y,width,height,startAngle, arcAngle); + } + + @Override + public void drawString(Object graphics, String str, int x, int y) { + g(graphics).drawString(str, x, y); + } + + @Override + public void drawShape(Object graphics, Shape shape, Stroke stroke) { + g(graphics).drawShape(shape, stroke); + } + + @Override + public void fillShape(Object graphics, Shape shape) { + g(graphics).fillShape(shape); + } + + @Override + public boolean isShapeClipSupported(Object graphics) { + return true; + } + + + + @Override + public boolean isShapeSupported(Object graphics) { + return true; + } + + @Override + public boolean isTransformSupported() { + return true; + } + + @Override + public void concatenateTransform(Object t1, Object t2) { + ((JSAffineTransform)t1).concatenate((JSAffineTransform)t2); + } + + @Override + public void copyTransform(Object src, Object dest) { + ((JSAffineTransform)dest).copyFrom((JSAffineTransform)src); + } + + @Override + public Object makeTransformAffine(double m00, double m10, double m01, double m11, double m02, double m12) { + JSAffineTransform t = JSAffineTransform.Factory.getTranslateInstance(0, 0); + t.setTransform(m00, m10, m01, m11, m02, m12); + return t; + } + + @Override + public void setTransformAffine(Object nativeTransform, double m00, double m10, double m01, double m11, double m02, double m12) { + ((JSAffineTransform)nativeTransform).setTransform(m00, m10, m01, m11, m02, m12); + } + + @Override + public Object makeTransformIdentity() { + return JSAffineTransform.Factory.getTranslateInstance(0, 0); + } + + @Override + public void setTransformIdentity(Object transform) { + ((JSAffineTransform)transform).setToTranslation(0, 0); + } + + @Override + public Object makeTransformRotation(float angle, float x, float y, float z) { + return JSAffineTransform.Factory.getRotateInstance(angle, x, y); + } + + @Override + public void setTransformRotation(Object nativeTransform, float angle, float x, float y, float z) { + ((JSAffineTransform)nativeTransform).setToRotation(angle, x, y); + } + + + + @Override + public Object makeTransformScale(float scaleX, float scaleY, float scaleZ) { + return JSAffineTransform.Factory.getScaleInstance(scaleX, scaleY); + } + + @Override + public void setTransformScale(Object nativeTransform, float scaleX, float scaleY, float scaleZ) { + ((JSAffineTransform)nativeTransform).setToScale(scaleX, scaleY); + } + + + + @Override + public Object makeTransformTranslation(float translateX, float translateY, float translateZ) { + return JSAffineTransform.Factory.getTranslateInstance(translateX, translateY); + } + + @Override + public void setTransformTranslation(Object nativeTransform, float translateX, float translateY, float translateZ) { + ((JSAffineTransform)nativeTransform).setToTranslation(translateX, translateY); + } + + + + @Override + public Object makeTransformInverse(Object nativeTransform) { + return ((JSAffineTransform)nativeTransform).createInverse(); + } + + @Override + public void setTransformInverse(Object nativeTransform) throws Transform.NotInvertibleException { + ((JSAffineTransform)nativeTransform).copyFrom((JSAffineTransform)makeTransformInverse(nativeTransform)); + } + + @Override + public void transformTranslate(Object nativeTransform, float x, float y, float z) { + ((JSAffineTransform)nativeTransform).translate(x, y); + } + + @Override + public void transformScale(Object nativeTransform, float x, float y, float z) { + ((JSAffineTransform)nativeTransform).scale(x, y); + } + + @Override + public void transformRotate(Object nativeTransform, float angle, float x, float y, float z) { + JSAffineTransform t = (JSAffineTransform)nativeTransform; + //if (x != 0 || y != 0) { + // t.translate(x, y); + //} + t.rotate(angle, x, y); + //if (x != 0 || y != 0) { + // t.translate(-x, -y); + //} + } + + @Override + public boolean transformEqualsImpl(Transform t1, Transform t2) { + if (t2 == null || t1 == null) { + return t1 == t2; + } + JSAffineTransform at1 = (JSAffineTransform)t1.getNativeTransform(); + JSAffineTransform at2 = (JSAffineTransform)t2.getNativeTransform(); + return at1.isEqualTo(at2); + } + + @Override + public void transformPoint(Object nativeTransform, float[] in, float[] out) { + Float64Array jsIn = Float64Array.create(2); + jsIn.set(0, in[0]); + jsIn.set(1, in[1]); + Float64Array jsOut = Float64Array.create(2); + ((JSAffineTransform)nativeTransform).transform(jsIn, 0, jsOut, 0, 1); + out[0] = (float)jsOut.get(0); + out[1] = (float)jsOut.get(1); + + } + + @Override + public void setTransform(Object graphics, Transform transform) { + Transform existing = ((HTML5Graphics)graphics).getTransform(); + if (existing == null) { + existing = transform==null ? Transform.makeIdentity() : transform.copy(); + ((HTML5Graphics)graphics).setTransform(existing); + } else { + if (transform == null) { + existing.setIdentity(); + } else { + existing.setTransform(transform); + } + ((HTML5Graphics)graphics).setTransformChanged(); + ((HTML5Graphics)graphics).applyTransform(); + } + } + + @Override + public Transform getTransform(Object graphics) { + Transform existing = ((HTML5Graphics)graphics).getTransform(); + if (existing == null) { + return Transform.makeIdentity(); + } else { + return existing.copy(); + } + } + + @Override + public void getTransform(Object nativeGraphics, Transform t) { + Transform existing = ((HTML5Graphics)graphics).getTransform(); + if (existing == null) { + t.setIdentity(); + } else { + t.setTransform(existing); + } + } + + @Override + public boolean isTransformSupported(Object graphics) { + return true; + } + + @Override + public void rotate(Object nativeGraphics, float angle) { + ((HTML5Graphics)nativeGraphics).rotate(angle); + } + + @Override + public void rotate(Object nativeGraphics, float angle, int pivotX, int pivotY) { + ((HTML5Graphics)nativeGraphics).rotate(angle, pivotX, pivotY); + } + + + + @Override + public void shear(Object nativeGraphics, float x, float y) { + //((HTML5Graphics)nativeGraphics).shear(x, y); + throw new UnsupportedOperationException("Graphics.shear() not implemented yet"); + } + + @Override + public void scale(Object nativeGraphics, float x, float y) { + ((HTML5Graphics)nativeGraphics).scale(x, y); + } + + @Override + public boolean isAffineSupported() { + return true; + } + + @Override + public void resetAffine(Object nativeGraphics) { + ((HTML5Graphics)nativeGraphics).resetAffine(); + } + + + + @Override + public void drawImage(Object graphics, Object img, int x, int y) { + g(graphics).drawImage(img, x, y); + } + + @Override + public void drawImage(Object graphics, Object img, int x, int y, int w, int h) { + g(graphics).drawImage(img, x, y, w, h); + } + + // Tried to implement tiling but it is creating artifacts... low priority + // so leaving it for now. + @Override + public void tileImage(Object graphics, Object img, int x, int y, int w, int h) { + g(graphics).tileImage(img, x, y, w, h); + } + + + + @Override + public void drawRGB(Object graphics, int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { + + g(graphics).drawRGB(rgbData, offset, x, y, w, h, processAlpha); + } + + @Override + public Object getNativeGraphics() { + return graphics; + } + + @Override + public Object getNativeGraphics(Object image) { + return ((NativeImage)image).mutableGraphics; + } + + @Override + public int charsWidth(Object nativeFont, char[] ch, int offset, int length) { + return ((NativeFont)nativeFont).stringWidth(new String(ch, offset, length)); + } + + @Override + public int stringWidth(Object nativeFont, String str) { + //return graphics.stringWidth(nativeFont, str); + return ((NativeFont)nativeFont).stringWidth(str); + } + + @Override + public int charWidth(Object nativeFont, char ch) { + return ((NativeFont)nativeFont).charWidth(ch); + //return stringWidth(nativeFont, ch+""); + } + + + + @Override + public int getHeight(Object nativeFont) { + return ((NativeFont)nativeFont).fontHeight(); + + } + + + + @Override + public int getFontAscent(Object nativeFont) { + return g(graphics).getFontAscent(nativeFont); + } + + @Override + public boolean isBaselineTextSupported() { + return true; + } + + + + @Override + public int getFontDescent(Object nativeFont) { + return g(graphics).getFontDescent(nativeFont); + } + + @Override + public boolean isNativeFontSchemeSupported() { + return true; + } + + + + + @Override + public Object getDefaultFont() { + NativeFont f = new NativeFont(); + //f.css = defaultFont.css; + f.face = defaultFont.face; + f.size = defaultFont.size; + f.style = defaultFont.style; + f.height = defaultFont.height; + f.ascent = defaultFont.ascent; + f.fileName = defaultFont.fileName; + f.fontName = defaultFont.fontName; + return f; + } + + + @JSBody(params={"fontName", "dataUrl", "fontFormat"}, script="var newStyle = document.createElement('style');\n" + +"newStyle.appendChild(document.createTextNode(\"\\\n" + +"@font-face {\\\n" + +" font-family: '\" + cn1_escape_single_quotes(fontName) + \"';\\\n" + +" src: url('\" + cn1_escape_single_quotes(dataUrl) + \"') format('\" + cn1_escape_single_quotes(fontFormat) + \"');\\\n" + +"}\\\n" + +"\"));\n" + +"\n" + +"document.head.appendChild(newStyle);" + + "WebFont.load({" + + "custom:{families:[fontName]}, " + + "active:function(){document.dispatchEvent(new CustomEvent('fontLoaded', {detail:{fontName:fontName, success:true}}));}," + + "inactive:function(){document.dispatchEvent(new CustomEvent('fontLoaded', {detail:{fontName:fontName, success:false}}))}}); ") + private native static void loadTrueTypeFont_(String fontName, String dataUrl, String fontFormat); + + private Set loadedFonts = new HashSet(); + + @Override + public Object loadTrueTypeFont(String fontName, String fileName) { + if (fontName.indexOf("native:") != 0 && !loadedFonts.contains(fontName)) { + ArrayBufferInputStream is = (ArrayBufferInputStream)this.getResourceAsStream(null, fileName); + String dataURL = arrayBufferToDataURL(is.getBuffer().getBuffer(), "font/truetype"); + final boolean[] complete = new boolean[1]; + EventListener loadedListener = new EventListener() { + + @Override + public void handleEvent(Event evt) { + window.getDocument().removeEventListener("fontLoaded", this); + new Thread(new Runnable() { + public void run() { + synchronized(complete) { + complete[0] = true; + complete.notify(); + } + } + }).start(); + } + + }; + window.getDocument().addEventListener("fontLoaded", loadedListener); + loadTrueTypeFont_(fontName, dataURL, "truetype"); + synchronized(complete) { + while (!complete[0]) { + try { + complete.wait(1000); + } catch (InterruptedException ex) { + Log.e(ex); + } + } + } + + + + loadedFonts.add(fontName); + } + NativeFont out = (NativeFont)createFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM); + out.fontName = nativeFontName(fontName); + out.fileName = fileName; + if (fontName.startsWith("native:") && fontName.contains("Italic")) { + out.style = Font.STYLE_ITALIC; + } + return out; + + } + + private String nativeFontName(String fontName) { + if(fontName != null && fontName.startsWith("native:")) { + if("native:MainThin".equals(fontName)) { + return "native-MainThin"; + } + if("native:MainLight".equals(fontName)) { + return "native-MainLight"; + } + if("native:MainRegular".equals(fontName)) { + return "native-MainRegular"; + } + + if("native:MainBold".equals(fontName)) { + return "native-MainBold"; + } + + if("native:MainBlack".equals(fontName)) { + return "native-MainBlack"; + } + + if("native:ItalicThin".equals(fontName)) { + return "native-ItalicThin"; + } + + if("native:ItalicLight".equals(fontName)) { + return "native-ItalicLight"; + } + + if("native:ItalicRegular".equals(fontName)) { + return "native-ItalicRegular"; + } + + if("native:ItalicBold".equals(fontName) || "native:ItalicBlack".equals(fontName)) { + return "native-ItalicBold"; + } + } + return fontName; + } + + @Override + public Object deriveTrueTypeFont(Object font, float size, int weight) { + NativeFont f = (NativeFont)font; + NativeFont f2 = new NativeFont(); + + int fontstyle = Font.STYLE_PLAIN; + if ((weight & Font.STYLE_BOLD) != 0) { + fontstyle |= Font.STYLE_BOLD; + } + if ((weight & Font.STYLE_ITALIC) != 0) { + fontstyle |= Font.STYLE_ITALIC; + } + f2.face = f.face; + f2.size = f.size; + f2.style = fontstyle; + f2.height = size; + + f2.fontName = f.fontName; + f2.fileName = f.fileName; + return f2; + + } + + @Override + public boolean isTrueTypeSupported() { + return true; + } + + + @JSBody(params={}, script="var baseFont=window.getParameterByName('baseFont');" + + "if (baseFont) return parseInt(baseFont); else return 0;") + private native static int getBaseFontSize(); + + @JSBody(params={}, script="var density=window.getParameterByName('density');" + + "if (density) return parseInt(density); else return 0;") + private native static int getDensityOverride(); + + @Override + public Object createFont(int face, int style, int size) { + + int height = getBaseFontSize(); + if (height == 0) { + height = 16; + } + switch (getDeviceDensity()) { + case Display.DENSITY_LOW: + case Display.DENSITY_VERY_LOW: + height = height/2; break; + case Display.DENSITY_HIGH: + height = height + height/2; break; + case Display.DENSITY_VERY_HIGH: + height = height * 2; break; + case Display.DENSITY_HD: + height = height * 3; break; + case Display.DENSITY_560: + height = height * 4; break; + case Display.DENSITY_2HD: + height = height * 5; break; + case Display.DENSITY_4K: + height = height * 6; break; + } + int diff = height / 3; + + switch (size) { + case Font.SIZE_SMALL: + height -= diff; + break; + case Font.SIZE_LARGE: + height += diff; + break; + } + NativeFont f = new NativeFont(); + //f.css = height+"px Sans-serif"; + f.face = face; + f.style = style; + f.size = size; + f.height = height; + + return f; + } + + @Override + public int getFace(Object nativeFont) { + if (nativeFont == null){ + return Font.FACE_SYSTEM; + }; + return ((NativeFont)nativeFont).face; + } + + @Override + public int getSize(Object nativeFont) { + if (nativeFont == null){ + return Font.SIZE_MEDIUM; + } + return ((NativeFont)nativeFont).size; + } + + @Override + public int getStyle(Object nativeFont) { + if (nativeFont == null){ + return Font.STYLE_PLAIN; + } + return ((NativeFont)nativeFont).style; + } + + @Override + public Object connect(String url, boolean read, boolean write, int timeout) throws IOException { + return JavaScriptNetworkAdapter.connect(url, read, write, timeout, + urlProxifier == null ? null : new JavaScriptNetworkAdapter.UrlTransformer() { + @Override + public String transform(String input) { + return urlProxifier.proxifyURL(input); + } + }, + new JavaScriptNetworkAdapter.ConnectionFactory() { + @Override + public NetworkConnection create(String targetUrl, boolean readConnection, boolean writeConnection, int connectionTimeout) throws IOException { + return new NetworkConnection(targetUrl, readConnection, writeConnection, connectionTimeout); + } + }); + } + + + + + + + + + + @Override + public Object connect(String url, boolean read, boolean write) throws IOException { + + return connect(url, read, write, -1); + + } + + @Override + public void setHeader(Object connection, String key, String val) { + JavaScriptNetworkAdapter.setHeader((JavaScriptNetworkAdapter.Connection) connection, key, val); + } + + @Override + public void setHttpMethod(Object connection, String method) throws IOException { + JavaScriptNetworkAdapter.setHttpMethod((JavaScriptNetworkAdapter.Connection) connection, method); + } + + + + @Override + public int getContentLength(Object connection) { + return JavaScriptNetworkAdapter.getContentLength((JavaScriptNetworkAdapter.Connection) connection); + } + + @Override + public OutputStream openFileOutputStream(String file) throws IOException { + if (isTempFile(file)) { + throw new IOException("Temp file writing not supported yet."); + } + if (isDirectory(file)) { + throw new IOException("Failed to open output stream for "+file+" because it is a directory."); + } + return LocalForage.getInstance().openOutputStream(wrapFile(file), new ItemSavedListener(){ + + @Override + public void onSave(LocalForage.ItemSavedEvent evt) { + FileInfo finfo = createFileInfo(evt.getSize(), System.currentTimeMillis()); + try { + LocalForage.getInstance().setItem(evt.getKey()+".cn1fileinfo", finfo); + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error onSave in localForage"); + consoleLog(ex.getMessage()); + } + } + + }); + } + + + + + @Override + public OutputStream openOutputStream(Object connection) throws IOException { + return JavaScriptNetworkAdapter.openOutputStream(connection, new JavaScriptNetworkAdapter.FileOutputStreamProvider() { + @Override + public OutputStream openFileOutputStream(String file) throws IOException { + return HTML5Implementation.this.openFileOutputStream(file); + } + }); + } + + @Override + public OutputStream openOutputStream(Object connection, int offset) throws IOException { + throw new RuntimeException("getOutputStream with offset not supported yet"); + } + + @Override + public boolean shouldWriteUTFAsGetBytes() { + return true; + } + + + + @Override + public InputStream openInputStream(Object connection) throws IOException { + return JavaScriptNetworkAdapter.openInputStream(connection); + } + + @Override + public void cleanup(Object o) { + super.cleanup(o); + JavaScriptNetworkAdapter.cleanup(o); + } + + + + @Override + public void setPostRequest(Object connection, boolean p) { + JavaScriptNetworkAdapter.setPostRequest((JavaScriptNetworkAdapter.Connection) connection, p); + } + + @Override + public int getResponseCode(Object connection) throws IOException { + return JavaScriptNetworkAdapter.getResponseCode((JavaScriptNetworkAdapter.Connection) connection); + } + + @Override + public String getResponseMessage(Object connection) throws IOException { + return JavaScriptNetworkAdapter.getResponseMessage((JavaScriptNetworkAdapter.Connection) connection); + } + + @Override + public String getHeaderField(String name, Object connection) throws IOException { + return JavaScriptNetworkAdapter.getHeaderField(name, (JavaScriptNetworkAdapter.Connection) connection); + } + + @Override + public String[] getHeaderFieldNames(Object connection) throws IOException { + return JavaScriptNetworkAdapter.getHeaderFieldNames((JavaScriptNetworkAdapter.Connection) connection); + } + + @Override + public String[] getHeaderFields(String name, Object connection) throws IOException { + return JavaScriptNetworkAdapter.getHeaderFields(name, (JavaScriptNetworkAdapter.Connection) connection); + } + + private LocalForage getLocalForage(){ + return LocalForage.getInstance(); + } + + private String wrapStorageKey(String name){ + return JavaScriptRuntimeFacade.wrapStorageKey(name); + } + + private String unwrapStorageKey(String name){ + return JavaScriptRuntimeFacade.unwrapStorageKey(name); + } + + private String wrapFile(String path){ + return JavaScriptRuntimeFacade.wrapFile(path); + } + + private String unwrapFile(String path){ + return JavaScriptRuntimeFacade.unwrapFile(path); + } + + @Override + public void deleteStorageFile(String name) { + try { + JavaScriptStorageAdapter.deleteStorageFile(createStorageBackend(), name); + } catch (IOException ex){ + consoleLog("Error on deleteStorageFile "+name+", : "+ex.getMessage()); + + } + } + + + @Override + public OutputStream createStorageOutputStream(String name) throws IOException { + return JavaScriptStorageAdapter.createStorageOutputStream(createStorageBackend(), name); + } + + @Override + public InputStream createStorageInputStream(String name) throws IOException { + return JavaScriptStorageAdapter.createStorageInputStream(createStorageBackend(), name); + } + + @Override + public boolean storageFileExists(String name) { + try { + return JavaScriptStorageAdapter.storageFileExists(createStorageBackend(), name); + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error checking storage for storageFileExists"); + consoleLog(ex.getMessage()); + } + return false; + } + + @Override + public int getStorageEntrySize(String name) { + try { + return JavaScriptStorageAdapter.getStorageEntrySize(createStorageBackend(), name); + } catch (IOException ex) { + //Log.e(ex); + return -1; + } + } + + + + @Override + public String[] listStorageEntries() { + try { + return JavaScriptStorageAdapter.listStorageEntries(createStorageBackend()); + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error in listStorageEntries"); + consoleLog(ex.getMessage()); + } + return new String[]{}; + } + + private JavaScriptStorageAdapter.Backend createStorageBackend() { + return new JavaScriptStorageAdapter.Backend() { + @Override + public void removeItem(String key) throws IOException { + getLocalForage().removeItem(key); + } + + @Override + public OutputStream openOutputStream(String key) throws IOException { + return getLocalForage().openOutputStream(key); + } + + @Override + public InputStream openInputStream(String key) throws IOException { + return getLocalForage().openInputStream(key); + } + + @Override + public Object getItem(String key) throws IOException { + return getLocalForage().getItem(key); + } + + @Override + public int getSize(String key) throws IOException { + return getLocalForage().getSize(key); + } + + @Override + public String[] keys() throws IOException { + return getLocalForage().keys(); + } + }; + } + + @Override + public String[] listFilesystemRoots() { + return new String[]{"file:///"}; + } + + @Override + public String getAppHomePath() { + return listFilesystemRoots()[0]; + } + + + + @Override + public String[] listFiles(String directory) throws IOException { + String wrapped = stripTrailingSlash(wrapFile(directory))+"/"; + try { + List out = new ArrayList<>(); + String[] keys = getLocalForage().keys(); + for (String key : keys){ + if (!key.startsWith(wrapped) || key.endsWith(".cn1fileinfo")) { + continue; + } + String fileName = key; + int pos = fileName.lastIndexOf("/"); + if (pos >= 0) { + fileName = fileName.substring(pos+1); + } + + if (key.equals(wrapped + fileName)) { + if (fileName.endsWith(".cn1dir")) { + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + } + out.add(fileName); + } + } + + return out.toArray(new String[out.size()]); + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error in listFiles"); + consoleLog(ex.getMessage()); + } + return new String[]{}; + } + + @Override + public long getRootSizeBytes(String root) { + return -1; + } + + @Override + public long getRootAvailableSpace(String root) { + return -1; + } + + @Override + public void mkdir(String directory) { + String wrapped = stripTrailingSlash(wrapFile(directory))+".cn1dir"; + if (!this.exists(directory)) { + try { + LocalForage.getInstance().setItem(wrapped, ""); + } catch (IOException ex) { + //Log.e(ex); + consoleLog("error in mkdir"); + consoleLog(ex.getMessage()); + } + } else { + consoleLog("Directory "+directory+" already exists"); + } + } + + @Override + public void deleteFile(String file) { + if (isTempFile(file)) { + deleteTempFile(file); + return; + } + String wrapped = stripTrailingSlash(wrapFile(file)); + boolean isDirectory = this.isDirectory(file); + if (isDirectory) { + if (isRootFile(file)) { + consoleLog("Failed to delete file "+file+" because it is the root directory"); + return; + } + try { + String[] children = listFiles(file); + if (children.length != 0) { + consoleLog("Failed to delete directory "+file+" because it is not empty"); + return; + } + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error in deleteFile"); + consoleLog(ex.getMessage()); + return; + } + try { + consoleLog("Attempting to delete directory "+file+" - wrapped=>" + wrapped); + LocalForage.getInstance().removeItem(wrapped+".cn1dir"); // actually delete the entry + } catch (IOException ex) { + consoleLog("Error in deleteFile"); + consoleLog(ex.getMessage()); + } + return; + } + + try { + LocalForage lf = LocalForage.getInstance(); + + lf.removeItem(wrapped); + lf.removeItem(wrapped+".cn1fileinfo"); + } catch (IOException ex) { + consoleLog("Error in deleteFile"); + consoleLog(ex.getMessage()); + } + + } + + @Override + public boolean isHidden(String file) { + return false; + } + + @Override + public void setHidden(String file, boolean h) { + + } + + /** + * Schema for info object that stores info about a file in the file system. + * These are stored as javascript objects under the name .cn1fileinfo + * in the same directory as . + */ + private static interface FileInfo extends JSObject { + @JSProperty + public double getFileLength(); + + @JSProperty + public double getLastModified(); + + @JSProperty + public void setFileLength(double len); + + @JSProperty + public void setLastModified(double timestamp); + } + + + @JSBody(params={"fileLength", "lastModified"}, script="return {fileLength: fileLength, lastModified: lastModified}") + private static native FileInfo createFileInfo(double len, double modified); + + @JSBody(params={"blob"}, script="window.cn1TmpFiles = window.cn1TmpFiles || []; return window.cn1TmpFiles.push(blob)-1;") + private native static int createTempFile_(Blob blob); + + @JSBody(params={"index"}, script="window.cn1TmpFiles = window.cn1TmpFiles || []; return window.cn1TmpFiles[index];") + private native static Blob getTempFile_(int index); + + @JSBody(params={"index"}, script="window.cn1TmpFiles = window.cn1TmpFiles || []; window.cn1TmpFiles[index] = null;") + private static native void deleteTempFile_(int index); + + @JSBody(params={"blob"}, script="return blob.name || null;") + private static native String getFileName(Blob blob); + + public static boolean isTempFile(String path) { + return path.indexOf("tmp://") == 0; + } + + private static int getTempFileIndex(String path) { + int lastPos = path.lastIndexOf("/"); + if (lastPos < 0 || lastPos >= path.length()-1) return -1; + int startPos = lastPos+1; + int endPos = path.indexOf("-", startPos); + if (endPos < 0) { + endPos = path.length(); + } + + return Integer.parseInt(path.substring(startPos, endPos)); + } + + public static Blob getTempFile(String path) { + int index = getTempFileIndex(path); + if (index < 0) return null; + return getTempFile_(index); + } + + public static String createTempFile(Blob blob) { + int index = createTempFile_(blob); + String filename = getFileName(blob); + return "tmp://"+index+(filename==null ? "" : ("-"+filename)); + } + + public static void deleteTempFile(String path) { + int index = getTempFileIndex(path); + if (index < 0) return; + deleteTempFile_(index); + } + + /** + * Gets the FileInfo object for a given file. Returns a file info object + * with -1 length and -1 mod time if there was an error retrieving the info. + * @param file The full path tot he file to get the info for. + * @return + */ + private FileInfo getFileInfo(String file) { + if (isTempFile(file)) { + Blob b = getTempFile(file); + if (b == null) { + return createFileInfo(-1,-1); + } + return createFileInfo(b.getSize(), -1); + } + if (isDirectory(file)) { + return getFileInfo(stripTrailingSlash(file)+".cn1dir"); + } + String wrapped = stripTrailingSlash(wrapFile(file)); + try { + return (FileInfo) LocalForage.getInstance().getItem(wrapped+".cn1fileinfo"); + } catch (IOException ex) { + //Log.e(ex); + return createFileInfo(-1, -1); + } + } + + @Override + public long getFileLength(String file) { + return (long) getFileInfo(file).getFileLength(); + + } + + @Override + public InputStream openFileInputStream(String file) throws IOException { + if (isTempFile(file)) { + Blob b = getTempFile(file); + if (b == null) { + throw new IOException("Failed to find temp file "+file); + } + return BlobUtil.openInputStream(b); + } + String wrapped = stripTrailingSlash(wrapFile(file)); + if (!exists(file)) { + throw new IOException("Failed to open input stream for file "+file+" because it does not exist."); + } + + if (isDirectory(file)) { + throw new IOException("Failed to open input stream for file "+file+" because it is a directory."); + } + + return LocalForage.getInstance().openInputStream(wrapped); + } + + @JSBody(params={"o", "type"}, script="return (o instanceof window[type]);") + private static native boolean instanceOf(JSObject o, String type); + + + private Blob openFileAsBlob(String file) throws IOException { + + if (!exists(file)) { + throw new IOException("Failed to open input stream for file "+file+" because it does not exist."); + } + + if (isDirectory(file)) { + throw new IOException("Failed to open input stream for file "+file+" because it is a directory."); + } + + JSObject obj = null; + if (isTempFile(file)) { + obj = getTempFile(file); + } else { + String wrapped = stripTrailingSlash(wrapFile(file)); + obj = LocalForage.getInstance().getItem(wrapped); + } + if (instanceOf(obj, "Blob")) { + return (Blob)obj; + } else if (instanceOf(obj, "Uint8Array")) { + return BlobUtil.createBlob((Uint8Array)obj, "application/octet-stream"); + } else { + throw new IOException("File at "+file+" is not a blob"); + } + } + + private static String stripTrailingSlash(String path) { + return JavaScriptRuntimeFacade.stripTrailingSlash(path); + } + @Override + public boolean isDirectory(String file) { + if (isRootFile(file)) { + return true; + } + if (isTempFile(file)) { + return false; + } + try { + String wrapped = stripTrailingSlash(wrapFile(file)); + + return LocalForage.getInstance().getItem(wrapped+".cn1dir") != null; + } catch (IOException ex) { + //Log.e(ex); + return false; + } + } + + @Override + public boolean exists(String file) { + if (isRootFile(file)) { + return true; + } + if (isTempFile(file)) { + return getTempFile(file) != null; + } + try { + String wrapped = stripTrailingSlash(wrapFile(file)); + return LocalForage.getInstance().getItem(wrapped) != null || LocalForage.getInstance().getItem(wrapped+".cn1dir") != null; + } catch (IOException ex) { + //Log.e(ex); + return false; + } + } + + private boolean isRootFile(String file) { + return JavaScriptRuntimeFacade.isRootFile(file); + } + + /** + * + * @param file Full path to file to rename + * @param newName New name (file nmame only) + */ + @Override + public void rename(String file, String newName) { + rename(file, newName, true); + } + private void rename(String file, String newName, boolean checkParent) { + if (newName.indexOf('/') < 0) { + if(file.endsWith("/")) { + file = file.substring(0, file.length() - 1); + } + int pos = file.lastIndexOf('/'); + if(pos > -1) { + newName = file.substring(0, pos) + "/" + newName; + } + } + if (isRootFile(file) || isRootFile(newName)) { + System.out.println("Failed to rename file "+file+" to "+newName+". Cannot rename to root"); + return; + } + file = stripTrailingSlash(file); + newName = stripTrailingSlash(newName); + if (isTempFile(file)) { + System.out.println("Cannot rename temp files"); + return; + } + if (exists(newName)) { + System.out.println("Failed to rename "+file+" to "+newName+" because a file already exists with that name"); + return; + } + if (!exists(file)) { + System.out.println("Failed to rename "+file+" to "+newName+" because the source file does not exist."); + return; + } + String parent = newName.substring(0, newName.lastIndexOf('/')); + if (checkParent) { + if (!isRootFile(parent) && !exists(parent)) { + System.out.println("Cannot rename file "+file+" to "+newName+" because the parent directory "+parent+" does not exist"); + return; + } + + } + boolean isDirectory = isDirectory(file); + String wrappedInput = stripTrailingSlash(wrapFile(file)); + String wrappedOutput = stripTrailingSlash(wrapFile(newName)); + if (isDirectory) { + wrappedOutput += ".cn1dir"; + wrappedInput += ".cn1dir"; + } + + + + FileInfo finfo = getFileInfo(file); + LocalForage forage = LocalForage.getInstance(); + try { + + if (isDirectory) { + // We need to rename all of the children of directories explicitly + // because the localforage database stores each file in its own row with full path. + for (String child : listFiles(file)) { + rename(file + "/" + child, newName + "/" + child, false); + } + } + + JSObject contents = forage.getItem(wrappedInput); + forage.setItem(wrappedOutput, contents); + if (!isDirectory) { + forage.setItem(wrappedOutput+".cn1fileinfo", finfo); + } + forage.removeItem(wrappedInput); + if (!isDirectory) { + forage.removeItem(wrappedInput+".cn1fileinfo"); + } + + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error in rename"); + consoleLog(ex.getMessage()); + } + + + } + + @Override + public char getFileSystemSeparator() { + return '/'; + } + + @Override + public String getPlatformName() { + return "HTML5"; + } + + @JSBody(params={"str"}, script="return window.parseFloat(str)") + private native static double _parseDouble(String str); + + @JSBody(params={"n"}, script="return n.toLocaleString()") + private native static String _toLocaleString(double n); + + @JSBody(params={"n"}, script="return n.toLocaleString()") + private native static String _toLocaleString(int n); + + @JSBody(params={"locale", "n"}, script="return n.toLocaleString(locale)") + private native static String _toLocaleString(String locale, int n); + + @JSBody(params={"locale", "n"}, script="return n.toLocaleString(locale)") + private native static String _toLocaleString(String locale, double n); + + + @JSBody(params={}, script="var number=0;try {number.toLocaleString('i');} catch (e) { return e.name ==='RangeError';} return false") + private native static boolean _toLocaleStringSupportsLocales(); + + private static int toLocaleStringSupportsLocales=-1; + + private static boolean toLocaleStringSupportsLocales() { + if (toLocaleStringSupportsLocales == -1) { + toLocaleStringSupportsLocales = _toLocaleStringSupportsLocales() ? 1 : 0; + } + return toLocaleStringSupportsLocales == 1; + } + + private static String toLocaleString(String locale, int n) { + if (toLocaleStringSupportsLocales()) { + return _toLocaleString(locale, n); + } else { + return _toLocaleString(n); + } + } + + private static String toLocaleString(String locale, double n) { + if (toLocaleStringSupportsLocales()) { + return _toLocaleString(locale, n); + } else { + return _toLocaleString(n); + } + } + + + + + + JSNumberFormat numberFormatter; + Map currencyFormatters = new HashMap(); + Map dateFormatters = new HashMap(); + + @JSBody(params={}, script="return navigator.language || navigator.browserLanguage") + private static native String getBrowserLanguage(); + + @Override + public L10NManager getLocalizationManager() { + if (l10n == null) { + Locale l = Locale.getDefault(); + l10n = new L10NManager(l.getLanguage(), l.getCountry()) { + + private String thousandsSeparator; + private String decimalSeparator; + + private String decimalSeparator() { + if (decimalSeparator == null) { + String formatted = this.format(2.1); + for (char c : formatted.toCharArray()) { + if (Character.isDigit(c)) { + + } else { + decimalSeparator = String.valueOf(c); + break; + } + } + if (decimalSeparator == null) { + throw new RuntimeException("Cannot find decimal separator"); + } + } + return decimalSeparator; + } + + private String thousandsSeparator() { + if (thousandsSeparator == null) { + String formatted = this.format(1000); + for (char c : formatted.toCharArray()) { + if (Character.isDigit(c)) { + + } else { + thousandsSeparator = String.valueOf(c); + break; + } + } + if (thousandsSeparator == null) { + thousandsSeparator = ","; + } + } + return thousandsSeparator; + } + + private JSNumberFormat numberFormatter() { + if (numberFormatter == null) { + numberFormatter = new JSNumberFormat(this.getLocaleStr()); + } + return numberFormatter; + } + + private JSNumberFormat getCurrencyFormatter(String currency) { + JSNumberFormat fmt = currencyFormatters.get(currency+"-"+this.getLocaleStr()); + if (fmt == null) { + fmt = new JSNumberFormat(this.getLocaleStr()); + fmt.setStyle("currency"); + fmt.setCurrency(currency); + currencyFormatters.put(currency+"-"+this.getLocaleStr(), fmt); + } + + return fmt; + + } + + private JSDateFormat getDateFormatter(String style) { + JSDateFormat fmt = dateFormatters.get(style); + if (fmt == null) { + fmt = new JSDateFormat(this.getLocaleStr()); + fmt.setStyle(style); + dateFormatters.put(this.getLocaleStr(), fmt); + } + return fmt; + } + + @Override + public String getLongMonthName(Date date) { + java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("MMMM", Locale.getDefault()); + return fmt.format(date); + } + + @Override + public String getShortMonthName(Date date) { + java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("MMM", Locale.getDefault()); + return fmt.format(date); + } + + private String getLocaleStr() { + Locale l = Locale.getDefault(); + return l.getLanguage()+"-"+l.getCountry(); + } + + public double parseDouble(String localeFormattedDecimal) { + localeFormattedDecimal = StringUtil.replaceAll(localeFormattedDecimal, thousandsSeparator(), ""); + localeFormattedDecimal = localeFormattedDecimal.replace(decimalSeparator().charAt(0), '.'); + double out = _parseDouble(localeFormattedDecimal); + return out; + + } + + public String format(int number) { + return _toLocaleString(number); + } + + public String format(double number) { + return _toLocaleString(number); + } + + public String formatCurrency(double currency) { + + return getCurrencyFormatter(Display.getInstance().getProperty("l10n.currency", "USD")).format(currency); + } + + public String formatDateLongStyle(Date d) { + return getDateFormatter(JSDateFormat.DATE_LONG).format(d); + } + + public String formatDateShortStyle(Date d) { + return getDateFormatter(JSDateFormat.DATE_SHORT).format(d); + } + + public String formatDateTime(Date d) { + return getDateFormatter(JSDateFormat.DATETIME_LONG).format(d); + } + + public String formatDateTimeMedium(Date d) { + return getDateFormatter(JSDateFormat.DATETIME_MEDIUM).format(d); + } + + public String formatDateTimeShort(Date d) { + return getDateFormatter(JSDateFormat.DATETIME_SHORT).format(d); + } + + public String getCurrencySymbol() { + return Display.getInstance().getProperty("l10n.currency.symbol", "$"); + } + + + public void setLocale(String locale, String language) { + + super.setLocale(locale, language); + Locale l = new Locale(language, locale); + Locale.setDefault(l); + } + }; + } + return l10n; + } + + @Override + public boolean postMessage(Object browserPeer, String message, String targetOrigin) { + + ((HTML5BrowserComponent)browserPeer).postMessage(message, targetOrigin); + return true; + } + + @Override + public boolean installMessageListener(Object browserPeer) { + ((HTML5BrowserComponent)browserPeer).installMessageListener(); + return true; + } + + @Override + public boolean uninstallMessageListener(Object browserPeer) { + ((HTML5BrowserComponent)browserPeer).uninstallMessageListener(); + return true; + } + + + + + + @Override + public boolean isNativeBrowserComponentSupported() { + if (!"false".equals(Display.getInstance().getProperty("javascript.nativeBrowser", "true"))) { + return true; + } else { + return false; + } + } + + @JSBody(script="var el = jQuery('').get(0); el.parentNode.removeChild(el);return el") + private static native HTMLIFrameElement createBlankIFrame(); + + private class SystemBrowserComponent extends BrowserComponent { + + } + + @Override + protected BrowserComponent createSharedJavascriptContext() { + return new SystemBrowserComponent(); + } + + + + + @Override + public PeerComponent createBrowserComponent(Object browserComponent) { + if (browserComponent instanceof SystemBrowserComponent) { + return new HTML5BrowserComponent(null, browserComponent); + } + HTMLIFrameElement el = (HTMLIFrameElement)window.getDocument().createElement("iframe"); + //HTMLIFrameElement el = createBlankIFrame(); + + HTML5BrowserComponent browser = new HTML5BrowserComponent(el, browserComponent); + return browser; + } + + @Override + public Object createNativeBrowserWindow(String startURL) { + return new HTML5BrowserWindow(startURL, ""); + } + + @Override + public void nativeBrowserWindowShow(Object window) { + ((HTML5BrowserWindow)window).show(); + } + + @Override + public void nativeBrowserWindowCleanup(Object window) { + ((HTML5BrowserWindow)window).cleanup(); + } + + @Override + public void nativeBrowserWindowSetSize(Object window, int width, int height) { + ((HTML5BrowserWindow)window).setSize(width, height); + } + + @Override + public void nativeBrowserWindowSetTitle(Object window, String title) { + ((HTML5BrowserWindow)window).setTitle(title); + } + + @Override + public void nativeBrowserWindowHide(Object window) { + ((HTML5BrowserWindow)window).hide(); + } + + @Override + public void nativeBrowserWindowAddCloseListener(Object window, ActionListener l) { + ((HTML5BrowserWindow)window).addCloseListener(l); + } + + @Override + public void nativeBrowserWindowRemoveCloseListener(Object window, ActionListener l) { + ((HTML5BrowserWindow)window).removeCloseListener(l); + } + + @Override + public void addNativeBrowserWindowOnLoadListener(Object window, ActionListener l) { + ((HTML5BrowserWindow)window).addLoadListener(l); + } + + @Override + public void removeNativeBrowserWindowOnLoadListener(Object window, ActionListener l) { + ((HTML5BrowserWindow)window).removeLoadListener(l); + } + + @Override + public void nativeBrowserWindowEval(Object window, BrowserWindow.EvalRequest req) { + ((HTML5BrowserWindow)window).eval(req); + } + + + + + + + + + + + + @Override + public void browserExecute(PeerComponent browserPeer, String javaScript) { + ((HTML5BrowserComponent)browserPeer).execute(javaScript); + } + + public boolean supportsBrowserExecuteAndReturnString(PeerComponent internal) { + return true; + } + + @Override + public String browserExecuteAndReturnString(PeerComponent internal, String javaScript) { + return ((HTML5BrowserComponent)internal).executeAndReturnString(javaScript); + } + + @Override + public boolean browserHasBack(PeerComponent browserPeer) { + return ((HTML5BrowserComponent)browserPeer).hasBack(); + } + + @Override + public void browserBack(PeerComponent browserPeer) { + ((HTML5BrowserComponent)browserPeer).back(); + } + + + + @Override + public boolean browserHasForward(PeerComponent browserPeer) { + return ((HTML5BrowserComponent)browserPeer).hasForward(); + } + + @Override + public void browserForward(PeerComponent browserPeer) { + ((HTML5BrowserComponent)browserPeer).forward(); + } + + @Override + public void browserClearHistory(PeerComponent browserPeer) { + + } + + + + @Override + public void browserDestroy(PeerComponent internal) { + + } + + @Override + public void browserStop(PeerComponent browserPeer) { + + } + + @Override + public Object createSoftWeakRef(Object o) { + if (Display.getInstance().getProperty("javascript.useES6WeakRefs", "true").equals("true") && isWeakMapSupported()) { + if (o == null) { + return new JSObjectWrapper(); + } + JSObject key = createSoftWeakRefImpl((JSObject)o); + JSObjectWrapper keyOut = new JSObjectWrapper(); + keyOut.o = key; + return keyOut; + } else { + return super.createSoftWeakRef(o); + } + } + + @Override + public Object extractHardRef(Object o) { + if (Display.getInstance().getProperty("javascript.useES6WeakRefs", "true").equals("true") && isWeakMapSupported()) { + if (o==null || !(o instanceof JSObjectWrapper)) { + return null; + } + JSObjectWrapper w = (JSObjectWrapper)o; + + return w.o == null ? null : extractHardRefImpl(w.o); + } else { + return super.extractHardRef(o); + } + } + + + + private static class JSObjectWrapper { + JSObject o; + } + + @JSBody(params={}, script="return window.WeakMap !== undefined;") + private static native boolean isWeakMapSupported(); + + @JSBody(params={"o"}, script="var key={}; window.cn1GlobalWeakMap.set(key, o); return key;") + private native static JSObject createSoftWeakRefImpl(JSObject o); + + @JSBody(params={"key"}, script="return window.cn1GlobalWeakMap.has(key) ? window.cn1GlobalWeakMap.get(key) : null") + private native static JSObject extractHardRefImpl(JSObject key); + + @JSBody(params={}, script="return window.cn1IsPreview === true") + private native static boolean isPreview_(); + + @Override + public void setBrowserPageInHierarchy(PeerComponent browserPeer, String url) throws IOException { + if (url.length() > 0 && url.charAt(0) != '/') { + url = "/" + url; + + } + setBrowserURL(browserPeer, "assets/cn1html"+url); + } + + + + @Override + public void setBrowserURL(PeerComponent browserPeer, String url) { + if (url.startsWith("jar:")) { + url = url.substring(6); + while (url.indexOf("/") == 0) { + url = url.substring(1); + } + + String currPath = window.getLocation().getPathName(); + String dirPath = currPath; + if (dirPath.indexOf("/") != -1){ + dirPath = dirPath.substring(0, dirPath.lastIndexOf("/")); + } + + + url = ((WindowLocation)window.getLocation()).getOrigin()+ + dirPath + "/assets/" + url; + + if (isPreview_()) { + try { + InputStream resource = Display.getInstance().getResourceAsStream(null, url.substring(url.lastIndexOf("/"))); + String str = Util.readToString(resource); + Util.cleanup(resource); + setBrowserPage(browserPeer, str, url); + return; + } catch (IOException ex) { + //Log.e(ex); + consoleLog("Error in setBrowserURL"); + consoleLog(ex.getMessage()); + } + } + + + } else if (url.startsWith("file://")) { + + } + ((HTML5BrowserComponent)browserPeer).setURL(url); + } + + @Override + public void setBrowserPage(PeerComponent browserPeer, String html, String baseUrl) { + ((HTML5BrowserComponent)browserPeer).setPage(html, baseUrl); + } + + @Override + public void setBrowserProperty(PeerComponent browserPeer, String key, Object value) { + ((HTML5BrowserComponent)browserPeer).setProperty(key, value); + } + + @Override + public String getBrowserURL(PeerComponent browserPeer) { + return ((HTML5BrowserComponent)browserPeer).getURL(); + } + + @Override + public String getBrowserTitle(PeerComponent browserPeer) { + return "Untitled Page"; + } + + @Override + public void browserReload(PeerComponent browserPeer) { + ((HTML5BrowserComponent)browserPeer).reload(); + } + + private String[] mediaExtensions = new String[]{ + "mp4", "mpg", "mov", "aiff", "mp3", "mpeg" + }; + private boolean isMediaResource(String resource){ + for (String ext : mediaExtensions){ + if (resource.endsWith("."+ext)){ + return true; + } + } + return false; + } + + public InputStream getStream(String url){ + return getArrayBufferInputStream(url); + + + } + + static String arrayBufferToDataURL(ArrayBuffer buf, String type){ + return "data:"+type+";base64,"+((WindowExt)Window.current()).arrayBufferToBase64(buf); + } + + static String blobToDataURL(Blob blob){ + return BlobUtil.blobToBase64(blob); + } + /* + static void blobToDataURL(Blob blob, final AsyncCallback callback){ + ((WindowExt)JS.getGlobal()).BlobToBase64(blob, new DataURLCallback(){ + + @Override + public void callback(String str) { + callback.complete(str); + } + + }); + } + */ + + private String buildVersion; + + @JSBody(script="return jQuery('html').attr('data-cn1-app-version')") + private native static String getBuildVersion_(); + + public String getBuildVersion() { + if (buildVersion == null) { + buildVersion = getBuildVersion_(); + } + if (buildVersion == null) { + buildVersion = Display.getInstance().getProperty("AppVersion", "1.0"); + } + return buildVersion; + } + + @JSBody(script="try {history.pushState(\"jibberish\", null, null)} catch (e){console.log('history.pushState not supported. Back command will not work.')}") + private native static void pushHistoryState(); + + @Override + public void setCurrentForm(Form f) { + super.setCurrentForm(f); + pushHistoryState(); + + } + + + + public InputStream getArrayBufferInputStream(String url){ + String dataURL = ((WindowExt)window).getCn1().getBundledAssetAsDataURL(url); + if (dataURL != null) { + Blob blob = ((WindowExt)window).Base64ToBlob(dataURL); + ArrayBufferInputStream out; + try { + out = new ArrayBufferInputStream(BlobUtil.toUint8Array(blob), "application/octet-stream"); + return out; + } catch (IOException ex) { + ex.printStackTrace(); + } + + } + + if (isMediaResource(url)){ + ArrayBufferInputStream out = new ArrayBufferInputStream(Uint8Array.create(0), null); + out.setSrc(url); + return out; + } + final XMLHttpRequest req = XMLHttpRequest.create(); + final Object lock = new Object(); + final boolean[] complete = new boolean[1]; + if (url.indexOf("assets/") == 0 && url.indexOf("?") == -1) { + url = url + "?v=" + getBuildVersion(); + } + req.open("get", url, true); + req.setOnReadyStateChange(new ReadyStateChangeHandler() { + + @Override + public void stateChanged() { + if ( req.getReadyState() == XMLHttpRequest.DONE ){ + + new Thread() { + @Override + public void run() { + complete[0]=true; + synchronized(lock){ + lock.notifyAll(); + } + } + }.start(); + } + } + }); + req.setResponseType("arraybuffer"); + + req.send(); + + while (!complete[0]){ + synchronized(lock){ + try { + lock.wait(); + } catch (InterruptedException ex) { + //Log.e(ex); + } + } + } + + if (req.getResponse() == null ){ + System.out.println(req.getAllResponseHeaders()); + System.out.println(req.getStatusText()); + System.out.println("Failed to load resource "+url); + System.out.println("Status code was "+req.getStatus()); + return null; + } + + ArrayBufferInputStream out = new ArrayBufferInputStream(Uint8Array.create((ArrayBuffer)req.getResponse()), req.getResponseType()); + return out; + } + + + @JSBody(params={"resource"}, script="cn1LoadedFile(resource)") + private native static void notifyProgressLoaderThatResourceIsLoaded(String resource); + + @Override + public InputStream getResourceAsStream(Class cls, String resource) { + int lastSlash = resource.lastIndexOf("/"); + if ( lastSlash >= 0 ){ + resource = resource.substring(lastSlash+1); + } + if (!"icon.png".equals(resource)) { + resource = "assets/"+resource; + } + InputStream out = getStream(resource); + notifyProgressLoaderThatResourceIsLoaded(resource); + return out; + + } + + @JSBody(script="jQuery(\"div#cn1-splash\").fadeOut(100, function(){ jQuery(this).remove(); });") + private native static void hideSplash(); + + @JSBody(script="window.setTimeout(function(){if (!window.loadServiceWorker) return; window.loadServiceWorker()}, 1000);") + private native static void loadServiceWorker(); + + @Override + public void confirmControlView() { + super.confirmControlView(); + hideSplash(); + loadServiceWorker(); + } + + public static void registerSaveBlobToFile() { + BlobUtil.registerNativeBlobToFileConverter(); + } + + private NativeImage createNativeImage(byte[] bytes, int offset, int len){ + Uint8Array arr = Uint8Array.create(len); + for (int i=0; i getTargetTouches(); + + @JSProperty + int getClientX(); + + @JSProperty + int getClientY(); + + } + + + interface Uint8ClampedArraySetter extends JSObject { + void set(Uint8ClampedArray arr); + } + + Map> charWidthCache = new HashMap>(); + Map> stringWidthCache = new HashMap>(); + private static final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghizjlmnopqrstuvwxyz12345678980"; + + // Note that we use a special case for the font height of Material fonts to + // try to optimize the result for FontImages and rotation. + // https://github.com/codenameone/CodenameOne/issues/2631 + @JSBody(params={"fontStyle"}, script="if (fontStyle.indexOf('Material') != -1) {\n" + + "try {\n" + +" var out = parseInt(/[0-9.]+(?=pt|px)/.exec(fontStyle));\n" + +" return out;\n" + +" } catch (e){}\n" + + "}\n" + + "window.cn1_font_height_cache = window.cn1_font_height_cache || {};\n" + +"var result = window.cn1_font_height_cache[fontStyle];\n" + + "var dummy = window.cn1_font_height_dummy;\n" + + "if (!dummy) {\n"+ + " dummy = document.createElement('div');\n" + + " var dummyText = document.createTextNode('M');\n"+ + " dummy.appendChild(dummyText);\n"+ + " window.cn1_font_height_dummy=dummy;\n"+ + " document.getElementsByTagName('body')[0].appendChild(dummy);\n"+ + "}\n"+ + +"\n" + +"if (!result)\n" + +"{\n" + + "dummy.style.display='';\n" + +" dummy.setAttribute('style', 'font:' + fontStyle + ';position:absolute;top:0;left:0;padding:0;border:none;line-height:1.2');\n" + +" result = dummy.offsetHeight;\n" + +"\n" + +" window.cn1_font_height_cache[fontStyle] = result;\n" + +" dummy.style.display='none'; document.getElementById('codenameone-canvas').setAttribute('tabindex', '0'); document.getElementById('codenameone-canvas').focus();\n" + +"}\n" + +"\n" + +"return result;") + private native static double determineFontHeight(String fontStyle); + + @JSBody(params={"fontStyle"}, script="window.cn1_font_leading_cache = window.cn1_font_leading_cache || {};\n" + +"var result = window.cn1_font_leading_cache[fontStyle];\n" + + "var dummy = window.cn1_font_height_dummy;\n" + + "if (!dummy) {\n"+ + " dummy = document.createElement('div');\n" + + " var dummyText = document.createTextNode('M');\n"+ + " dummy.appendChild(dummyText);\n"+ + " window.cn1_font_height_dummy=dummy;\n"+ + " document.getElementsByTagName('body')[0].appendChild(dummy);\n"+ + "}\n"+ + +"\n" + +"if (!result)\n" + +"{\n" + + "dummy.style.display='';\n" + +" dummy.setAttribute('style', 'font:' + fontStyle + ';position:absolute;top:0;left:0;padding:0;border:none;line-height:1.2');\n" + +" var fontSize = parseFloat(window.getComputedStyle(dummy, null).getPropertyValue('font-size'));\n" ++ "result = dummy.offsetHeight - fontSize;\n" + +"\n" + +" window.cn1_font_leading_cache[fontStyle] = result;\n" + +" dummy.style.display='none'; document.getElementById('codenameone-canvas').setAttribute('tabindex', '0'); document.getElementById('codenameone-canvas').focus();\n" + +"}\n" + +"\n" + +"return result;") + private native static double determineFontLeading(String fontStyle); + + @JSBody(params={}, script="return window.cn1_use_baseline_text_rendering || false;") + public native static boolean useBaselineTextRendering(); + + public class NativeFont { + //String css; + int face; + int style; + int size; + int ascent; + double height; + String fileName; + String fontName; + + String cssCached_, cssFontFamilyCached__; + + public int fontLeading() { + return (int)Math.ceil(determineFontLeading(getCSS())); + } + + public String getCSSFontFamily() { + if (cssFontFamilyCached__ == null) { + StringBuilder sb = new StringBuilder(); + if (fontName != null) { + if (fontName.startsWith("native-")) { + sb.append(fontFamily()).append(", "); + } else { + sb.append("'").append(fontName).append("'").append(", "); + } + } + switch (face) { + case Font.FACE_SYSTEM: + sb.append("sans-serif"); + break; + case Font.FACE_PROPORTIONAL: + sb.append("serif"); + break; + case Font.FACE_MONOSPACE: + sb.append("monospace"); + break; + + } + cssFontFamilyCached__ = sb.toString(); + } + return cssFontFamilyCached__; + } + + private String fontStyle() { + + + if ("native-ItalicThin".equals(fontName) || "native-ItalicLight".equals(fontName) || "native-ItalicRegular".equals(fontName) || "native-ItalicBold".equals(fontName) || (style & Font.STYLE_ITALIC) != 0) { + return "italic"; + } + + return ""; + } + + private String fontFamily() { + if (fontName.startsWith("native-")) { + if ("native-MainThin".equals(fontName) || "native-ItalicThin".equals(fontName)) { + return "'HelveticaNeue-UltraLight', 'HelveticaNeue UltraLight', Sans-serif"; + } + if ("native-MainLight".equals(fontName) || "native-ItalicLight".equals(fontName)) { + return "'HelveticaNeue-Thin', 'HelveticaNeue Thin', Sans-serif"; + } + if ("native-MainRegular".equals(fontName) || "native-ItalicRegular".equals(fontName)) { + return "'HelveticaNeue-Medium', 'HelveticaNeue Medium', Sans-serif"; + } + if ("native-MainBold".equals(fontName) || "native-ItalicBold".equals(fontName)) { + return "'HelveticaNeue-Bold', 'HelveticaNeue Bold', Sans-serif"; + } + if ("native-MainBlack".equals(fontName) || "native-ItalicBlack".equals(fontName)) { + return "'HelveticaNeue-Black', 'HelveticaNeue Black', Sans-serif"; + } + } + return fontName; + } + + private String fontWeight() { + + if ("native-MainThin".equals(fontName)) { + /*font-weight:100; + font-stretch:condensed;*/ + return "100"; + } + + + + + if ("native-MainLight".equals(fontName)) { + /*font-weight:300; + font-stretch:condensed;*/ + return "300"; + } + + if ("native-MainRegular".equals(fontName)) { + /*font-weight:500; + font-stretch:normal;*/ + return "500"; + } + + if ("native-MainBold".equals(fontName)) { + /*font-weight:600; + font-stretch:normal;*/ + return "600"; + + } + + if ("native-MainBlack".equals(fontName)) { + /*font-weight:800; + font-stretch:condensed;*/ + return "800"; + } + + if ("native-ItalicThin".equals(fontName)) { + /*font-weight:100; + font-stretch:condensed; + font-style: italic;*/ + return "100"; + } + + if ("native-ItalicLight".equals(fontName)) { + return "300"; + } + + if ("native-ItalicRegular".equals(fontName)) { + return "500"; + } + + if ("native-ItalicBold".equals(fontName)) { + return "600"; + } + + if ((style & Font.STYLE_BOLD) != 0) { + return "bold"; + } + + return ""; + } + + public String getCSS(){ + if (cssCached_ == null) { + StringBuilder sb = new StringBuilder(); + //sb.append(height).append("px "); + //if ((style & Font.STYLE_ITALIC) != 0 || fontName != null && fontName.contains("Italic")) { + // sb.append("italic "); + //} + sb.append(fontStyle()).append(" "); + sb.append(fontWeight()).append(" "); + //if ((style & Font.STYLE_BOLD) != 0 || fontName != null && fontName.contains("Bold")){ + // sb.append("bold "); + //} + if (((int)height) ==0) { + height = defaultFont.height; + } + sb.append(height).append("px/1.0 "); + sb.append(getCSSFontFamily()); + cssCached_ = sb.toString(); + + } + return cssCached_; + } + + public String getScaledCSS(){ + + StringBuilder sb = new StringBuilder(); + //sb.append(height).append("px "); + //if ((style & Font.STYLE_ITALIC) != 0) { + // sb.append("italic "); + //} + sb.append(fontStyle()).append(" "); + //if ((style & Font.STYLE_BOLD) != 0 ){ + // sb.append("bold "); + //} + sb.append(fontWeight()).append(" "); + if (((int)height) ==0) { + height = defaultFont.height; + } + sb.append(scaleCoord(height)).append("px/1.0 "); + sb.append(getCSSFontFamily()); + //if (fontName != null) { + // sb.append(fontName).append(", "); + //} + //switch (face) { + // case Font.FACE_SYSTEM: + // sb.append("sans-serif"); + // break; + // case Font.FACE_PROPORTIONAL: + // sb.append("serif"); + // break; + // case Font.FACE_MONOSPACE: + // sb.append("monospace"); + // break; + + //} + return sb.toString(); + + } + + public String toString(){ + return getCSS()+" (Face: "+face+" style "+style+" size "+size; + } + + + + public int charWidth(char c){ + Map cache = charWidthCache.get(getCSS()); + if (cache == null){ + cache = new HashMap(); + charWidthCache.put(getCSS(), cache); + } + Character ch = new Character(c); + Integer i = cache.get(ch); + if (i != null){ + return i.intValue(); + } + int w = graphics.charsWidth(this, new char[]{c},0,1); + cache.put(ch, new Integer(w)); + return w; + } + + public int stringWidth(String str){ + if (str.length() < 50){ + Map cache = stringWidthCache.get(getCSS()); + if (cache == null){ + cache = new HashMap(); + stringWidthCache.put(getCSS(), cache); + } + + Integer i = cache.get(str); + if (i != null){ + return i.intValue(); + } + int w = graphics.stringWidth(this, str); + cache.put(str, new Integer(w)); + return w; + } else { + return graphics.stringWidth(this, str); + } + } + + + public int fontHeight(){ + //return (int)Math.round(height); + //return graphics.getFontHeight(this); + return (int)Math.ceil(determineFontHeight(getCSS())); + /* + Integer h = fontHeightCache.get(css); + if (h == null){ + int height = graphics.getFontHeight(this); + h = new Integer(height); + fontHeightCache.put(css, h); + } + return h.intValue(); + */ + + + } + + public int fontAscent() { + if (ascent == 0) { + HTMLCanvasElement canvas = getCanvasBuffer(100,100); + CanvasRenderingContext2D context = (CanvasRenderingContext2D)canvas.getContext("2d"); + String oldFont = context.getFont(); + context.setFont(((NativeFont)this).getCSS()); + //this.canvas.getStyle().setProperty("font", nativeFont+""); + //ascent = (int)Math.round(((JSOImplementations.JSFontMetrics)context.measureText(alphabet)).getAscent()); + ascent = (int)((fontHeight()-fontLeading()) * measureAscent(getCSSFontFamily())); + //context.setFont(oldFont); + + } + return ascent; + } + + + } + + @JSBody(params={"fontFamily"}, script="return window.measureTextAscent(fontFamily);") + native static double measureAscent(String fontFamily); + @JSBody(params={"fontFamily"}, script="return window.measureTextDescent(fontFamily);") + native static double measureDescent(String fontFamily); + + static void _log(String str){ + ((WindowExt)instance.window).getConsole().log(str); + } + + + @JSBody(params={"obj"}, script="console.log(obj)") + native static void _logInt(int obj); + + + + + + @Override + public Database openOrCreateDB(String databaseName) throws IOException { + WebSQL.Database db = WebSQL.openDatabase(databaseName, "1.0", databaseName, defaultFileSystemSize); + return new DatabaseImpl(db); + } + + @Override + public void startThread(String name, final Runnable r) { + + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + r.run(); + } catch (Exception ex) { + CodenameOneThread.handleException(ex); + } + } + + + }, name); + t.start(); + } + + + + + + + private static HTMLButtonElement clickBtn; + + public static void showButton(String text, final EventListener onClick) { + final Window win = Window.current(); + EventListener l = new EventListener() { + + @Override + public void handleEvent(Event evt) { + + if (clickBtn != null) { + clickBtn.getParentNode().removeChild(clickBtn); + clickBtn = null; + } + onClick.handleEvent(evt); + } + + }; + + HTMLButtonElement btn = showButton_(text, l); + btn.getStyle().setProperty("position", "absolute"); + btn.getStyle().setProperty("top", "0"); + btn.getStyle().setProperty("left", "0"); + btn.getStyle().setProperty("width", ""+scaleCoord(HTML5Implementation.instance.canvas.getWidth())+"px"); + btn.getStyle().setProperty("height", ""+scaleCoord(HTML5Implementation.instance.canvas.getHeight())+"px"); + btn.getStyle().setProperty("padding", "0"); + btn.getStyle().setProperty("margin", "0"); + btn.getStyle().setProperty("font-size", "2em"); + btn.getStyle().setProperty("opacity", "0.7"); + + clickBtn = btn; + } + + @JSBody(params={"label","l"}, script="return jQuery('').click(l).appendTo(jQuery('body')).get(0);") + private native static HTMLButtonElement showButton_(String label, EventListener l); + + @Override + public void registerPush(Hashtable metaData, boolean noFallback) { + HTML5Push.registerPush(); + } + + void _sendPushRegistrationError(String message, int errorCode) { + sendPushRegistrationError(message, errorCode); + } + + void _registerServerPush(String id) { + if (registerServerPush(id, getApplicationKey(), (byte)10, "", getPackageName())) { + super.sendRegisteredForPush(id); + } else { + super.sendPushRegistrationError("Failed to register server push", 0); + } + } + + void _pushReceived(String data) { + super.pushReceived(data); + } + + private static interface BeforeInstallPromptEvent extends Event { + public void prompt(); + + @JSProperty + public UserChoiceResultPromise getUserChoice(); + } + + private static interface UserChoiceResultPromise extends JSObject { + public void then(UserChoiceCallback callback); + } + + @JSFunctor + private static interface UserChoiceCallback extends JSObject { + public void onResult(UserChoiceResult result); + } + + private static interface UserChoiceResult extends JSObject { + @JSProperty + public String getOutcome(); + } + + private static final String USER_CHOICE_OUTCOME_ACCEPTED="accepted"; + + private BeforeInstallPromptEvent deferredPromptForInstall; + + @Override + public boolean canInstallOnHomescreen() { + return deferredPromptForInstall != null; + } + + @Override + public boolean promptInstallOnHomescreen() { + if (!canInstallOnHomescreen()) { + return false; + } + final boolean[] res = new boolean[1]; + if (!CN.isEdt()) { + + CN.callSeriallyAndWait(new Runnable() { + public void run() { + res[0] = promptInstallOnHomescreen(); + } + }); + return res[0]; + } + + // Now we know we're on the EDT + + if (canInstallOnHomescreen()) { + CN.invokeAndBlock(new Runnable() { + public void run() { + final boolean[] result = new boolean[1]; + final boolean[] complete = new boolean[1]; + deferredPromptForInstall.prompt(); + deferredPromptForInstall.getUserChoice().then(new UserChoiceCallback() { + @Override + public void onResult(final UserChoiceResult choice) { + new Thread(new Runnable() { + public void run() { + deferredPromptForInstall = null; + + if (USER_CHOICE_OUTCOME_ACCEPTED.equals(choice.getOutcome())) { + result[0] = true; + } + synchronized(complete) { + complete[0] = true; + complete.notifyAll(); + } + } + }).start(); + + + } + + }); + + while (!complete[0]) { + synchronized(complete) { + try { + complete.wait(); + } catch (Throwable t){} + } + } + res[0] = result[0]; + + } + + }); + + + } + return res[0]; + } + + + + @Override + public void onCanInstallOnHomescreen(final Runnable r) { + Window.current().addEventListener("beforeinstallprompt", new EventListener() { + @Override + public void handleEvent(BeforeInstallPromptEvent evt) { + deferredPromptForInstall = evt; + evt.preventDefault(); + new Thread(new Runnable() { + public void run() { + CN.callSerially(r); + } + }).start(); + } + + }, true); + } + + @JSBody(params={"onComplete"}, script="if (!document.body.requestFullscreen) return false; document.body.requestFullscreen().then(function(){onComplete(true);}).catch(function(err){onComplete(false)}); return true;") + private native static boolean requestFullScreen_(RequestFullScreenCallback onComplete); + + @JSFunctor + private static interface RequestFullScreenCallback extends JSObject { + public void onComplete(boolean result); + + } + + @JSBody(params={}, script="return document.fullscreenElement ? true:false") + private native static boolean isFullScreen_(); + + @JSBody(params={}, script="return document.body.requestFullscreen ? true : false") + private native static boolean isFullScreenSupported_(); + + @Override + public boolean isFullScreenSupported() { + return isFullScreenSupported_(); + } + + + + @Override + public boolean requestFullScreen() { + if (isFullScreen_()) return true; + if (!isFullScreenSupported_()) return false; + + final boolean[] complete = new boolean[1]; + final boolean[] res = new boolean[1]; + Button goFullScreenBtn = new Button("Click to Enter Fullscreen Mode"); + goFullScreenBtn.setMaterialIcon(FontImage.MATERIAL_FULLSCREEN); + EventListener l = new EventListener() { + @Override + public void handleEvent(Event evt) { + requestFullScreen_(new RequestFullScreenCallback() { + @Override + public void onComplete(final boolean result) { + new Thread(new Runnable() { + public void run() { + res[0] = result; + complete[0] = true; + synchronized(complete) { + try { + complete.notifyAll(); + } catch (Throwable t) { + + } + } + } + }).start(); + + } + }); + } + + }; + showNativeButton(goFullScreenBtn, l); + + CN.invokeAndBlock(new Runnable() { + public void run() { + while (!complete[0]) { + synchronized(complete) { + try { + complete.wait(); + } catch (Throwable t){} + } + } + + } + }); + Log.p("Result of full-screen request was "+res[0]); + return res[0]; + + } + + @JSBody(params={"onComplete"}, script="if (!document.exitFullscreen) {onComplete(false); return;} document.exitFullscreen().then(function(){onComplete(true)}).catch(function(e){onComplete(false);});") + private native static void exitFullscreen_(RequestFullScreenCallback onComplete); + + @Override + public boolean exitFullScreen() { + if (!isInFullScreenMode()) return true; + if (!isFullScreenSupported_()) return true; + final boolean[] complete = new boolean[1]; + final boolean[] res = new boolean[1]; + exitFullscreen_(new RequestFullScreenCallback() { + @Override + public void onComplete(final boolean result) { + new Thread(new Runnable() { + public void run() { + res[0] = result; + complete[0] = true; + synchronized(complete) { + try { + complete.notifyAll(); + } catch (Throwable t) { + + } + } + } + }).start(); + } + + }); + CN.invokeAndBlock(new Runnable() { + public void run() { + while (!complete[0]) { + synchronized(complete) { + try { + complete.wait(); + } catch (Throwable t){} + } + } + + } + }); + return res[0]; + } + + + + @Override + public boolean isInFullScreenMode() { + return isFullScreen_(); + } + + private HTMLButtonElement nativeBtn; + public void showNativeButton(Button nativeButton, final EventListener eventHandler) { + EventListener l = new EventListener() { + + @Override + public void handleEvent(Event evt) { + if (nativeBtn != null) { + nativeBtn.getParentNode().removeChild(nativeBtn); + nativeBtn = null; + } + eventHandler.handleEvent(evt); + evt.preventDefault(); + evt.stopPropagation(); + } + }; + HTMLButtonElement btn = showButton_("", l); + btn.getStyle().setProperty("position", "absolute"); + btn.getStyle().setProperty("top", "0"); + btn.getStyle().setProperty("left", "0"); + btn.getStyle().setProperty("width", ""+scaleCoord(canvas.getWidth())+"px"); + btn.getStyle().setProperty("height", ""+scaleCoord(canvas.getHeight())+"px"); + btn.getStyle().setProperty("padding", "0"); + btn.getStyle().setProperty("margin", "0"); + //btn.getStyle().setProperty("font-size", "2em"); + btn.getStyle().setProperty("opacity", "0.85"); + + //((NativeImage)im.getImage()).load(); + //_logObj(((NativeImage)im.getImage()).getImg()); + nativeButton.setWidth(nativeButton.getPreferredW()); + nativeButton.setHeight(nativeButton.getPreferredH()); + HTMLCanvasElement cv = ((HTML5Implementation.NativeImage)nativeButton.toImage().getImage()).getMutableGraphics().getCanvas(); + cv.getStyle().setProperty("width", scaleCoord(nativeButton.getWidth())+"px"); + cv.getStyle().setProperty("height", scaleCoord(nativeButton.getHeight())+"px"); + btn.appendChild(cv); + //btn.appendChild(((NativeImage)(nativeButton.toImage().getImage())).mutableGraphics.getCanvas()); + //btn.click(); + nativeBtn = btn; + + } + + + public static HTMLInputElement createButton(String cssClass, String label) { + HTMLDocument d = (HTMLDocument)Window.current().getDocument(); + HTMLInputElement el = (HTMLInputElement)d.createElement("button"); + //HTMLElement i = d.createElement("i"); + //i.setAttribute("class", "fa "+fontAwesomeClass+" fa-"+size); + el.setAttribute("class", cssClass); + + //HTMLElement span = d.createElement("span"); + el.appendChild(d.createTextNode(label)); + //el.appendChild(i); + //el.appendChild(span); + return el; + + } + + @JSBody(params={}, script="return (window.screen && window.screen.orientation && window.screen.orientation.lock) ? true : false") + private native static boolean supportsScreenOrientation_(); + + @JSBody(params={"type"}, script="window.screen.orientation.lock(type)") + private native static void lockOrientation_(String type); + + @Override + public boolean canForceOrientation() { + return isInFullScreenMode() && supportsScreenOrientation_(); + } + + @Override + public void lockOrientation(boolean portrait) { + if (canForceOrientation()) { + lockOrientation_(portrait ? "portrait" : "landscape"); + } else { + Log.p("lockOrientation not supported currently. lockOrientation is only supported in some devices, and only when running in full-screen mode."); + } + } + + @Override + public void unlockOrientation() { + if (canForceOrientation()) { + lockOrientation_("any"); + } + } + + @JSBody(params={}, script="return (location.protocol == 'https:' && navigator.share !== undefined)") + private native static boolean isNavigatorShareSupported_(); + + @Override + public boolean isNativeShareSupported() { + return isNavigatorShareSupported_(); + } + + @JSBody(params={"url"}, script="navigator.share({text:'', url:url})") + private native static void shareURL_(String url); + + @JSBody(params={"text"}, script="navigator.share({text:text, url:''})") + private native static void shareText_(String text); + + @JSBody(params={"text", "link"}, script="navigator.share({text:text, url:link})") + private native static void shareTextAndLink_(String text, String link); + + @Override + public void share(final String text, final String image, String mimeType, Rectangle sourceRect) { + if (isNavigatorShareSupported_()) { + confirmDialog("Confirm Share", "The application has requested to share some content. Click continue to proceed to the sharing dialog.", FontImage.MATERIAL_SHARE, "Continue", "Cancel", new JSRunnable() { + @Override + public void run() { + if (text != null && image != null && (image.startsWith("http://") || image.startsWith("https://"))) { + shareTextAndLink_(text, image); + return; + } + if (text != null && (text.startsWith("http://") || text.startsWith("https://"))) { + if (text.indexOf(" ") != -1) { + String url = text.substring(0, text.indexOf(" ")); + String message = text.substring(url.length()+1); + shareTextAndLink_(message, url); + return; + } + shareURL_(text); + return; + } + if (text != null) { + shareText_(text); + return; + } + if (image != null && (image.startsWith("http://") || image.startsWith("https://"))) { + shareURL_(image); + return; + } + + } + }); + } else { + super.share(text, image, mimeType, sourceRect); + } + } + + + private static interface CancelableEvent extends Event { + @JSProperty + public boolean isDefaultPrevented(); + } + + + + /** + * Creates a native confirm dialog that will run the given onOk runnable if the user clicks OK. This is + * handy for things that need to be triggered by user interaction. + * @param title Dialog title + * @param message Message + * @param icon Icon for the dialog + * @param ok OK button label + * @param cancel Cancel button label + * @param onOk Callback to be run on JS main thread when user clicks OK. + * @return + */ + private boolean confirmDialog(String title, String message, char icon, String ok, String cancel, final JSRunnable onOk) { + InteractionDialog dlg = new InteractionDialog(); + dlg.setLayout(new BorderLayout()); + dlg.setTitle(title); + dlg.add(BorderLayout.CENTER, new SpanLabel(message)); + FontImage img = FontImage.createMaterial(icon, new Label().getStyle(), 15f); + dlg.add(BorderLayout.WEST, new Label(img)); + final Button okBtn = new Button(ok); + final Button cancelBtn = new Button(cancel); + dlg.add(BorderLayout.SOUTH, FlowLayout.encloseRight(cancelBtn, okBtn)); + dlg.setAnimateShow(true); + final int[] okBounds = new int[4]; + final int[] cancelBounds = new int[4]; + + // Containually update the bounds of the ok and cancel buttons so that we know + // where they are for the native listener + UITimer t = UITimer.timer(200, true, new Runnable() { + @Override + public void run() { + copyBounds(okBtn, okBounds); + copyBounds(cancelBtn, cancelBounds); + } + + }); + + dlg.showPopupDialog(CN.getCurrentForm()); + + final boolean complete[] = new boolean[1]; + final boolean result[] = new boolean[1]; + EventListener clickListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + MouseEvent mevt = (MouseEvent)evt; + int px = getClientX(mevt); + int py = getClientY(mevt); + + if (contains(okBounds, px, py)) { + // This click was on the ok buttn + complete[0] = true; + result[0] = true; + if (onOk != null) { + onOk.run(); + } + new Thread(new Runnable() { + public void run() { + synchronized(complete) { + complete.notify(); + } + } + }).start(); + + } + + if (contains(cancelBounds, px, py)) { + // This click was on the ok buttn + complete[0] = true; + result[0] = false; + new Thread(new Runnable() { + public void run() { + synchronized(complete) { + complete.notify(); + } + } + }).start(); + + } + } + + }; + registerNativeClickHandler(clickListener); + + + CN.invokeAndBlock(new Runnable() { + @Override + public void run() { + while (!complete[0]) { + synchronized(complete) { + try { + complete.wait(); + } catch (Throwable t){} + } + } + } + + }); + dlg.disposeToTheBottom(); + t.cancel(); + unregisterNativeClickHandler(clickListener); + return result[0]; + } + + private static void copyBounds(Component cmp, int[] destBounds) { + destBounds[0] = cmp.getAbsoluteX(); + destBounds[1] = cmp.getAbsoluteY(); + destBounds[2] = cmp.getWidth(); + destBounds[3] = cmp.getHeight(); + } + + private static boolean contains(int[] bounds, int x, int y) { + return bounds[0] <= x && bounds[2] + bounds[0] >= x && bounds[1] <= y && bounds[1] + bounds[3] >= y; + } + + private void registerNativeClickHandler(final EventListener l) { + nativeEventListener = new EventListener() { + @Override + public void handleEvent(Event evt) { + if (evt.isCancelable()) { + evt.preventDefault(); + } + } + + }; + Window.current().getDocument().getBody().addEventListener("pointerup", l); + + } + + private void unregisterNativeClickHandler(EventListener l) { + nativeEventListener = null; + //Window.current().removeEventListener("click", l); + Window.current().getDocument().getBody().removeEventListener("pointerup", l); + } + + private String selectedText; + private ActionListener textSelectionListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + selectedText = ((TextSelection)t.getSource()).getSelectionAsText(); + outputCanvas.focus(); + } + + }; + + @JSBody(params={"textArea", "valueForClipboard"}, script="var range = document.createRange();\n" + +" range.selectNodeContents(textArea);\n" + +"\n" + +" var selection = window.getSelection();\n" + +" selection.removeAllRanges(); // remove previously selected ranges\n" + +" selection.addRange(range);\n" + +" textArea.setSelectionRange(0, valueForClipboard.length); ") + private static native void selectTextAreaIOS(HTMLTextAreaElement textArea, String valueForClipboard); + + @Override + public void copySelectionToClipboard(TextSelection sel) { + if (selectedText == null || selectedText.isEmpty()) { + return; + } + copyToClipboard(selectedText); + + } + + @JSBody(params={"name"}, script="return document.execCommand(name)") + private native static boolean execCommand(String name); + + + private class ClipboardCopyRequest{ + Object content; + boolean triedBacksideHook, triedInSheet; + ClipboardCopyRequest(Object content) { + this.content = content; + } + + } + @JSBody(params={"command"}, script="if (!document.queryCommandEnabled) return true; return document.queryCommandEnabled(command);") + private native static boolean queryCommandEnabled(String command); + + @Override + public void copyToClipboard(Object obj) { + final ClipboardCopyRequest request = (obj instanceof ClipboardCopyRequest) ? (ClipboardCopyRequest)obj : new ClipboardCopyRequest(obj); + obj = request.content; + super.copyToClipboard(obj); + if (!(obj instanceof String)) { + return; + } + String selectedText = (String)obj; + HTMLDocument doc = Window.current().getDocument(); + HTMLTextAreaElement textArea = (HTMLTextAreaElement)doc.createElement("textarea"); + textArea.setAttribute("readonly", ""); + doc.getBody().appendChild(textArea); + textArea.setValue(selectedText); + if (isIOS()) { + selectTextAreaIOS(textArea, selectedText); + } else { + textArea.select(); + } + boolean res = queryCommandEnabled("copy"); + if (res) { + execCommand("copy"); + } + doc.getBody().removeChild(textArea); + if (!res && !request.triedBacksideHook) { + // The copy failed + // let's try to add a backside hook + if (isBacksideHookAvailable()) { + addBacksideHook(new JSRunnable() { + @Override + public void run() { + request.triedBacksideHook = true; + copyToClipboard(request); + } + }); + } else { + request.triedBacksideHook = true; + copyToClipboard(request); + } + } else if (!res && !request.triedInSheet) { + // No backside hooks available + callSerially(new Runnable() { + @Override + public void run() { + final Sheet sheet = new Sheet(Sheet.getCurrentSheet(), "Copy to Clipboard"); + SpanLabel message = new SpanLabel(CN.getProperty("AppName", "This application")+" has requested to copy content to the system clipboard."); + sheet.getContentPane().setLayout(new BorderLayout()); + Button copy = new Button("Allow"); + Button cancel = new Button("Cancel"); + sheet.getContentPane().add(BorderLayout.CENTER, message); + sheet.getContentPane().add(BorderLayout.SOUTH, GridLayout.encloseIn(2, cancel, copy)); + copy.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + request.triedInSheet = true; + copyToClipboard(request); + sheet.back(); + } + }); + + cancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + sheet.back(); + } + }); + sheet.setPosition(BorderLayout.NORTH, BorderLayout.CENTER); + sheet.show(); + + } + + }); + } else if (!res) { + ToastBar.showErrorMessage("Failed to copy to clipboard due to browser permission restrictions.", 5000); + } + + } + + + + public String getSelectedText() { + return selectedText; + } + + @JSBody(params={"evt", "content"}, script="try { evt.clipboardData.setData('text/plain', content);} catch (e){}") + private native static void setClipboardData(Event evt, String content); + + EventListener copyListener = new EventListener() { + @SuppressSyncErrors + public void handleEvent(Event evt) { + if (selectedText == null || selectedText.isEmpty()) { + return; + } + if (!jQuery_is_(outputCanvas, ":focus")) { + return; + } + setClipboardData(evt, selectedText); + evt.preventDefault(); + } + }; + + EventListener selectAll = new EventListener() { + @SuppressSyncErrors + public void handleEvent(Event evt) { + if (jQuery_is_(outputCanvas, ":focus")) { + callSerially(new Runnable() { + public void run() { + Form f = CN.getCurrentForm(); + TextSelection sel = f.getTextSelection(); + if (sel.isEnabled()) { + sel.selectAll(); + } + } + }); + } + } + }; + private boolean contextListenerActive; + EventListener contextListener = new EventListener() { + @SuppressSyncErrors + public void handleEvent(final MouseEvent evt) { + _log("In context listener"); + if (!jQuery_is_(outputCanvas, ":focus")) { + return; + } + _log("focused"); + Form f = CN.getCurrentForm(); + if (f == null) { + return; + } + _log("Form is there"); + TextSelection sel = f.getTextSelection(); + if (sel == null || !sel.isEnabled()) { + return; + } + _log("Text selection is on"); + evt.preventDefault(); + callSerially(new Runnable() { + public void run() { + _log("Showing context menu"); + MouseEvent me = lastMouseEvent != null ? lastMouseEvent : evt; + ContextMenu.showAt(unscaleCoord(me.getClientX()) + CN.convertToPixels(2), unscaleCoord(me.getClientY()) + CN.convertToPixels(2)); + + } + }); + } + }; + + @Override + public void initializeTextSelection(TextSelection sel) { + sel.addTextSelectionListener(textSelectionListener); + HTMLDocument doc = Window.current().getDocument(); + doc.addEventListener("copy", copyListener); + contextListenerActive = true; + doc.addEventListener("contextmenu", contextListener); + + + + } + + @Override + public void deinitializeTextSelection(TextSelection sel) { + contextListenerActive = false; + HTMLDocument doc = Window.current().getDocument(); + doc.removeEventListener("copy", copyListener); + doc.removeEventListener("contextmenu", contextListener); + sel.removeTextSelectionListener(textSelectionListener); + } + + private class HeavyButton { + private HTMLInputElement el; + private Button btn; + + HeavyButton(Button btn, HTMLInputElement el) { + this.el = el; + this.btn = btn; + } + } + + @Override + public Object createHeavyButton(Button btn) { + HTMLInputElement nativeButton = createButton("heavy-btn", ""); + //nativeButton.appendChild(setStyleSize(((NativeImage)btn.toImage().getImage()).getMutableGraphics().getCanvas(), btn)); + return new HeavyButton(btn, nativeButton); + + } + + + Map heavyListeners = new HashMap(); + public void addHeavyActionListener(Object peer, final ActionListener l) { + HeavyButton hbtn = (HeavyButton)peer; + final HTMLInputElement el = hbtn.el; + EventListener eli = new EventListener() { + @SuppressSyncErrors + @Override + public void handleEvent(Event evt) { + l.actionPerformed(new ActionEvent(el)); + } + + }; + heavyListeners.put(l, eli); + el.addEventListener("click", eli); + } + + public void removeHeavyActionListener(Object peer, ActionListener l) { + HeavyButton hbtn = (HeavyButton)peer; + HTMLInputElement el = hbtn.el; + EventListener eli = heavyListeners.get(l); + if (eli != null) { + el.removeEventListener("click", eli); + } + } + + public void updateHeavyButtonBounds(Object peer, int x, int y, int width, int height) { + HeavyButton hbtn = (HeavyButton)peer; + HTMLInputElement el = hbtn.el; + el.getStyle().setProperty("top", scaleCoord(y)+"px"); + el.getStyle().setProperty("left", scaleCoord(x)+"px"); + el.getStyle().setProperty("width", scaleCoord(width)+"px"); + el.getStyle().setProperty("height", scaleCoord(height)+"px"); + } + + @Override + public void initHeavyButton(Object peer) { + HeavyButton hbtn = (HeavyButton)peer; + HTMLInputElement el = hbtn.el; + while (el.getFirstChild() != null) { + el.removeChild(el.getFirstChild()); + } + if (hbtn.btn.getWidth() == 0 || hbtn.btn.getHeight() == 0) { + hbtn.btn.setWidth(hbtn.btn.getPreferredW()); + hbtn.btn.setHeight(hbtn.btn.getPreferredH()); + } + if (hbtn.btn.getWidth() == 0 || hbtn.btn.getHeight() == 0) { + + } else { + el.appendChild(setStyleSize(((NativeImage)hbtn.btn.toImage().getImage()).getMutableGraphics().getCanvas(), hbtn.btn)); + } + Window.current().getDocument().getBody().appendChild(el); + } + + @Override + public void deinitializeHeavyButton(Object peer) { + HeavyButton hbtn = (HeavyButton)peer; + Window.current().getDocument().getBody().removeChild(hbtn.el); + super.deinitializeHeavyButton(peer); + } + + @Override + public boolean requiresHeavyButtonForCopyToClipboard() { + return true; + } + + + + + + private static HTMLCanvasElement setStyleSize(HTMLCanvasElement cv, Component cmp) { + + cv.getStyle().setProperty("width", scaleCoord(cmp.getWidth())+"px"); + cv.getStyle().setProperty("height", scaleCoord(cmp.getHeight())+"px"); + return cv; + } + + @JSBody(params={}, script="return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)") + private static native boolean isDarkMode_(); + + @Override + public Boolean isDarkMode() { + return isDarkMode_(); + } + + + + + + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Keyboard.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Keyboard.java new file mode 100644 index 0000000000..03d7431510 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Keyboard.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.impl.VirtualKeyboardInterface; +import com.codename1.io.Log; +import com.codename1.ui.CN; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; + +/** + * + * @author shannah + */ +public class HTML5Keyboard implements VirtualKeyboardInterface { + private static boolean virtualKeyboardOpen; + + public static void install() { + Display.getInstance().setDefaultVirtualKeyboard(new HTML5Keyboard()); + if (HTML5Implementation.isAndroid_()) { + initVirtualKeyboardDetector_(new VirtualKeyboardVisibleHandlerImpl(), new VirtualKeyboardHiddenHandlerImpl()); + } + } + + @Override + public void setInputType(int i) { + Log.p("setInputType() not supported in this keyboard"); + } + + @Override + public String getVirtualKeyboardName() { + return "HTML5 Virtual Keybaord"; + } + + @Override + public void showKeyboard(boolean show) { + if (show) { + if (isVirtualKeyboardShowing()) { + return; + } + Form f = CN.getCurrentForm(); + if (f != null) { + Component focused = f.getFocused(); + if (focused != null && focused.isEditable() && focused.isEnabled() && focused.isVisible() && !focused.isEditing()) { + focused.startEditingAsync(); + } + } + } else { + if (!isVirtualKeyboardShowing()) { + return; + } + HTML5Implementation.getInstance().stopTextEditing(); + } + } + + @Override + public boolean isVirtualKeyboardShowing() { + if (HTML5Implementation.isAndroid_()) { + return virtualKeyboardOpen; + } else if (HTML5Implementation.isIOS()) { + return HTML5Implementation.getInstance().isNativeInputFieldFocused(); + } else { + return false; + } + } + + + @JSBody(params={"onVisible", "onHidden"}, script="virtualKeyboardDetector.init( { recentlyFocusedTimeoutDuration: 3000 } ); " + + "virtualKeyboardDetector.on( 'virtualKeyboardVisible', onVisible ); " + + "virtualKeyboardDetector.on( 'virtualKeyboardHidden', onHidden );") + private native static void initVirtualKeyboardDetector_(VirtualKeyboardVisibleHandler onVisible, VirtualKeyboardHiddenHandler onHidden); + + @JSFunctor + private static interface VirtualKeyboardVisibleHandler extends JSObject { + public void onVisible(); + } + private static class VirtualKeyboardVisibleHandlerImpl implements VirtualKeyboardVisibleHandler { + @Override + public void onVisible() { + new Thread(new Runnable() { + @Override + public void run() { + + virtualKeyboardOpen = true; + Display.getInstance().fireVirtualKeyboardEvent(true); + } + }).start(); + } + } + + @JSFunctor + private static interface VirtualKeyboardHiddenHandler extends JSObject { + public void onHidden(); + } + private static class VirtualKeyboardHiddenHandlerImpl implements VirtualKeyboardHiddenHandler { + + @Override + public void onHidden() { + new Thread(new Runnable() { + @Override + public void run() { + virtualKeyboardOpen = false; + Display.getInstance().fireVirtualKeyboardEvent(false); + } + }).start(); + } + } + + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5LocationManager.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5LocationManager.java new file mode 100644 index 0000000000..62c96b339f --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5LocationManager.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.impl.html5.JSOImplementations.ErrorCallback; +import com.codename1.impl.html5.JSOImplementations.Geolocation; +import com.codename1.impl.html5.JSOImplementations.Geolocation.PositionOptions; +import com.codename1.impl.html5.JSOImplementations.PositionCallback; +import com.codename1.impl.html5.JSOImplementations.WindowExt; +import com.codename1.location.Location; +import com.codename1.location.LocationListener; +import com.codename1.location.LocationManager; +import com.codename1.ui.Display; +import java.io.IOException; +import org.teavm.interop.Async; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.browser.Window; +import org.teavm.jso.core.JSString; +import org.teavm.interop.AsyncCallback; + +/** + * + * @author shannah + */ +public class HTML5LocationManager extends LocationManager { + Location lastKnownLocation; + int listenerId; + + public HTML5LocationManager(){ + + } + + @Override + public Location getCurrentLocation() throws IOException { + lastKnownLocation = nativeGetCurrentLocation(); + return lastKnownLocation; + } + + @JSBody(params={"o"}, script="console.log(o)") + private static native void log(JSObject o); + + private static void log(String str) { + log(JSString.valueOf(str)); + } + + @Async + private static native Location nativeGetCurrentLocation() throws IOException; + + private static void nativeGetCurrentLocation(final AsyncCallback callback){ + //log("In nativeGetCurrentLocation"); + PositionOptions opts = (PositionOptions)((WindowExt)Window.current()).createEmptyObject(); + opts.setTimeout(5000); + Geolocation geo = ((WindowExt)Window.current()).getNavigator().getGeolocation(); + PositionCallback onPosition = new PositionCallback() { + + @Override + public void onLocation(final Geolocation.Position position) { + //log("IN onLocation callback with position"); + //log(position); + //new Thread(){ + // public void run(){ + Location loc = new Location(); + loc.setLatitude(position.getCoords().getLatitude()); + loc.setLongitude(position.getCoords().getLongitude()); + loc.setAccuracy((float)position.getCoords().getAccuracy()); + loc.setAltitude(position.getCoords().getAltitude()); + loc.setDirection((float)position.getCoords().getHeading()); + loc.setTimeStamp(position.getTimestamp()); + loc.setVelocity((float)position.getCoords().getSpeed()); + //log("Completing callback"); + callback.complete(loc); + // } + //}.start(); + + } + }; + + ErrorCallback onError = new ErrorCallback() { + + @Override + public void onError(JSOImplementations.JSError error) { + //log("Error getting location"); + //log(error); + callback.error(new IOException(error.getMessage())); + } + + }; + + geo.getCurrentPosition(onPosition, onError, opts); + } + + @Override + public Location getLastKnownLocation() { + return lastKnownLocation; + } + + @Override + protected void bindListener() { + if (listenerId==0){ + PositionOptions opts = (PositionOptions)((WindowExt)Window.current()).createEmptyObject(); + + Geolocation geo = ((WindowExt)Window.current()).getNavigator().getGeolocation(); + + PositionCallback onLocation = new PositionCallback(){ + + @Override + public void onLocation(final Geolocation.Position position) { + new Thread(){ + public void run(){ + final LocationListener l = HTML5LocationManager.this.getLocationListener(); + int oldStatus = HTML5LocationManager.this.getStatus(); + HTML5LocationManager.this.setStatus(LocationManager.AVAILABLE); + + if (oldStatus!=LocationManager.AVAILABLE && l!=null){ + Display.getInstance().callSerially(new Runnable() { + + @Override + public void run() { + l.providerStateChanged(LocationManager.AVAILABLE); + } + + }); + } + + Location loc = new Location(); + loc.setLatitude(position.getCoords().getLatitude()); + loc.setLongitude(position.getCoords().getLongitude()); + loc.setAccuracy((float)position.getCoords().getAccuracy()); + loc.setAltitude(position.getCoords().getAltitude()); + loc.setDirection((float)position.getCoords().getHeading()); + loc.setTimeStamp(position.getTimestamp()); + loc.setVelocity((float)position.getCoords().getSpeed()); + lastKnownLocation=loc; + + if (l != null){ + Display.getInstance().callSerially(new Runnable(){ + + @Override + public void run() { + l.locationUpdated(lastKnownLocation); + } + + }); + } + + + } + }.start(); + + } + + }; + + ErrorCallback onError = new ErrorCallback(){ + + @Override + public void onError(JSOImplementations.JSError error) { + int oldStatus = HTML5LocationManager.this.getStatus(); + switch (error.getCode()){ + case 1: // Permission Denied + HTML5LocationManager.this.setStatus(LocationManager.OUT_OF_SERVICE); + break; + case 2: // Unavailable + HTML5LocationManager.this.setStatus(LocationManager.TEMPORARILY_UNAVAILABLE); + break; + case 3: // TIMEOUT + HTML5LocationManager.this.setStatus(LocationManager.TEMPORARILY_UNAVAILABLE); + break; + } + + if (oldStatus != HTML5LocationManager.this.getStatus()) { + final LocationListener l = HTML5LocationManager.this.getLocationListener(); + if (l!=null) { + new Thread(){ + public void run() { + Display.getInstance().callSerially(new Runnable() { + + @Override + public void run() { + l.providerStateChanged(HTML5LocationManager.this.getStatus()); + } + }); + } + }.start(); + } + } + } + + }; + + geo.getCurrentPosition(onLocation, onError, opts); + } + } + + @Override + protected void clearListener() { + if (listenerId!=0) { + Geolocation geo = ((WindowExt)Window.current()).getNavigator().getGeolocation(); + geo.clearWatch(listenerId); + } + } + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Media.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Media.java new file mode 100644 index 0000000000..ff57072c5a --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Media.java @@ -0,0 +1,1334 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import static com.codename1.impl.html5.HTML5Implementation._log; +import com.codename1.impl.html5.JSOImplementations.HTMLMediaElement; +import com.codename1.impl.html5.JSOImplementations.HTMLVideoElement; +import com.codename1.io.Log; +import com.codename1.io.Util; +import com.codename1.media.AbstractMedia; +import com.codename1.media.AsyncMedia; +import com.codename1.media.Media; +import com.codename1.ui.Button; +import com.codename1.ui.CN; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.FontImage; +import com.codename1.ui.Sheet; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.MessageEvent; +import com.codename1.ui.events.MessageEvent.PromptPromise; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; +import com.codename1.ui.util.EventDispatcher; +import com.codename1.util.AsyncResource; +import com.codename1.util.AsyncResult; +import com.codename1.util.SuccessCallback; +import java.util.ArrayList; +import java.util.List; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.html.HTMLElement; + +/** + * + * @author shannah + */ +public class HTML5Media extends AbstractMedia { + public static boolean microphoneActive; + private HTMLMediaElement el; + private final boolean isVideo; + + private MediaComponent component; + private List completionHandlers; + private StateInternal currentState = new StateInternal(); + private boolean pausePending; + private int pendingTime=-1; + + private Sheet playMediaSheet; + private final EventDispatcher stateListeners = new EventDispatcher(); + + private PlayRequestInternal pendingPlayRequest; + private PauseRequestInternal pendingPauseRequest; + + @JSBody(params={"el"}, script="if (window.cn1OnCreateMedia) window.cn1OnCreateMedia(el);") + private static native void onCreateMedia(HTMLMediaElement el); + + public HTML5Media(HTMLMediaElement el, boolean isVideo){ + onCreateMedia(el); + this.el = el; + this.el.setVolume(1.0); + this.el.setMuted(false); + this.el.setControls(true); // Default is to show controls + // use setHideNativeControls to hide them. + this.isVideo = isVideo; + if (isVideo) { + el.setAttribute("playsinline", ""); + } + final EventListener onPlay = new EventListener() { + @Override + public void handleEvent(Event evt) { + final StateInternal newState = new StateInternal(currentState); + newState.playing = true; + newState.paused = false; + new Thread(new Runnable(){ + public void run() { + setState(newState); + } + }).start(); + + } + + }; + final EventListener onPause = new EventListener() { + @Override + public void handleEvent(Event evt) { + final StateInternal newState = new StateInternal(currentState); + newState.playing = false; + newState.paused = true; + pausePending = false; + new Thread(new Runnable(){ + public void run() { + setState(newState); + } + }).start(); + + } + + }; + final EventListener onCanPlay = new EventListener() { + @Override + public void handleEvent(Event evt) { + final boolean seeking = (pendingTime > 0); + if (seeking) { + HTML5Media.this.el.setCurrentTime(pendingTime/1000.0); + pendingTime = 0; + } + final StateInternal newState = new StateInternal(currentState); + newState.canPlay = true; + new Thread(new Runnable(){ + public void run() { + if (seeking) { + waitWhileSeeking(5000); + } + setState(newState); + } + }).start(); + } + + }; + final EventListener onError = new EventListener() { + @Override + public void handleEvent(Event evt) { + if (HTML5Media.this.el == null) { + return; + } + + final String errorMessage = getErrorMessage(HTML5Media.this.el); + final int code = getErrorCode(HTML5Media.this.el); + new Thread(new Runnable(){ + public void run() { + fireMediaError(createMediaException(errorMessage, code)); + } + }).start(); + + } + + }; + final EventListener onEnd = new EventListener() { + @Override + public void handleEvent(Event evt) { + if (!currentState.paused) { + final StateInternal newState = new StateInternal(currentState); + newState.playing = false; + newState.paused = true; + new Thread(new Runnable(){ + public void run() { + setState(newState); + } + }).start(); + } + + } + + }; + + initState(); + this.el.addEventListener("play", onPlay); + this.el.addEventListener("pause", onPause); + this.el.addEventListener("canplay", onCanPlay); + this.el.addEventListener("error", onError); + this.el.addEventListener("ended", onEnd); + + HTML5Implementation.getInstance().mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + @Override + public void run(HTMLElement el) { + el.removeEventListener("play", onPlay); + el.removeEventListener("pause", onPause); + el.removeEventListener("canplay", onCanPlay); + el.removeEventListener("error", onError); + el.removeEventListener("ended", onEnd); + } + }); + } + + private static MediaErrorType getMediaErrorType(int code) { + switch (code) { + case 1: + return MediaErrorType.Aborted; + case 2: + return MediaErrorType.Network; + case 3: + return MediaErrorType.Decode; + case 4: + return MediaErrorType.SrcNotSupported; + default: + return MediaErrorType.Unknown; + + } + } + + private static MediaException createMediaException(String message, int code) { + return new MediaException(getMediaErrorType(code), message); + } + + private void initState() { + if (this.el == null) { + return; + } + if (this.el.getReadyState() >= 3) { + currentState.canPlay = true; + } + if (currentState.canPlay && el.getCurrentTime() > 0 && !el.isPaused() && !el.isEnded()) { + currentState.playing = true; + currentState.paused = false; + } + } + + private void fireStateChange(final StateInternal oldState, final StateInternal newState) { + new Thread(new Runnable() { + public void run() { + stateListeners.fireActionEvent(new StateChangeEventInternal(HTML5Media.this, oldState, newState)); + } + }).start(); + } + + + + private static class StateInternal { + private boolean playing, paused, canPlay; + + StateInternal() { + this(false, false, true); + } + + StateInternal(boolean canPlay, boolean playing, boolean paused) { + this.playing = playing; + this.paused = paused; + } + + StateInternal(StateInternal state) { + this.canPlay = state.canPlay; + this.playing = state.playing; + this.paused = state.paused; + } + + + } + + private void setState(StateInternal state) { + StateInternal oldState = new StateInternal(currentState); + currentState = state; + System.out.println("Setting state to "+state); + fireStateChange(oldState, new StateInternal(state)); + if (state.playing && !oldState.playing) { + fireMediaStateChange(AsyncMedia.State.Playing); + } else if (state.paused && !oldState.paused) { + fireMediaStateChange(AsyncMedia.State.Paused); + } + + } + + private static class StateChangeEventInternal extends ActionEvent { + private StateInternal oldState; + private StateInternal newState; + + StateChangeEventInternal(Object source, StateInternal oldState, StateInternal newState) { + super(source); + this.oldState = oldState; + this.newState = newState; + } + + } + + + public HTMLMediaElement getMediaElement() { + return el; + } + + public void addCompletionHandler(Runnable r) { + if (this.el == null) { + return; + } + if (completionHandlers == null) { + completionHandlers = new ArrayList(); + final EventListener endedListener = new EventListener(){ + + @Override + public void handleEvent(Event evt) { + if (!currentState.paused) { + final StateInternal newState = new StateInternal(currentState); + newState.playing = false; + newState.paused = true; + new Thread(new Runnable(){ + public void run() { + setState(newState); + } + }).start(); + } + new Thread(){ + public void run(){ + Display.getInstance().callSerially(new Runnable() { + public void run() { + List toRun = new ArrayList(completionHandlers); + for (Runnable handler : toRun) { + handler.run(); + } + } + }); + } + }.start(); + } + + }; + el.addEventListener("ended", endedListener); + HTML5Implementation.getInstance().mediaPool().addCleanupListener(new HTML5MediaPool.CleanupListener(el) { + @Override + public void run(HTMLElement theEl) { + theEl.removeEventListener("ended", endedListener); + } + }); + } + completionHandlers.add(r); + } + + public void removeCompletionHandler(Runnable r) { + if (completionHandlers != null) { + completionHandlers.remove(r); + } + } + + @Override + public Component getVideoComponent() { + if (component == null) { + component = new MediaComponent(); + } + return component; + } + + private static boolean playMethodReturnsPromise; + private static boolean playMethodReturnsPromiseChecked; + + @JSBody(params={}, script="try { var promise = document.createElement('video').play(); return promise !== undefined;} catch (e) {return false;}") + private native static boolean _playMethodReturnsPromise(); + private static boolean playMethodReturnsPromise() { + if (!playMethodReturnsPromiseChecked) { + playMethodReturnsPromiseChecked = true; + playMethodReturnsPromise = _playMethodReturnsPromise(); + //HTML5Implementation._log("Play method returns promise? "+playMethodReturnsPromise); + } + return playMethodReturnsPromise; + } + + /** + * Callback used for {@link #playCatch(com.codename1.impl.html5.JSOImplementations.HTMLMediaElement, com.codename1.impl.html5.HTML5Media.NotAllowedHandler) } + * to handle the case where media playback was not allowed. + */ + @JSFunctor + static interface NotAllowedHandler extends JSObject { + public void onNotAllowed(); + } + + @JSFunctor + static interface FailedToPlayHandler extends JSObject { + public void failedToPlay(String msg); + } + + + @JSBody(params={"el", "onNotAllowed", "failedToPlay"}, script="try {\n" + + "var promise = window.cn1Play ? window.cn1Play(el) : el.play(); \n" + + "if (promise !== undefined) {\n" + + " promise.then(function(){\n" + + " el.setAttribute('data-cn1-unlocked', 'true');\n" + + " console.log('HTML5Media#playCatch: Audio playback started. id='+el.getAttribute('cn1-audio-id'));\n" + + " });\n" + + "}\n" + + "promise.catch(function(err){\n" + + " if (err.name=='NotAllowedError'){\n" + + " onNotAllowed();\n" + + " } else {\n" + + " console.log('Failed to play media with id='+el.getAttribute('cn1-audio-id'), err, el);\n" + + " if (failedToPlay) {\n" + + " failedToPlay(''+err);\n" + + " }\n" + + " }\n" + + "});\n" + + "} catch (ex){\n" + + " console.log(ex);\n" + + " if (failedToPlay) {\n" + + " failedToPlay(''+err);\n" + + " }\n" + + "}") + private static native void playCatch(HTMLMediaElement el, NotAllowedHandler onNotAllowed, FailedToPlayHandler failedToPlay); + + /** + * Plays media but catches any exceptions. Only used when the browser doesn't support + * media.play() returning promises. See compatibility chart for returning promises + * at https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play + * @param el + */ + @JSBody(params={"el"}, script="try {el.play()} catch (ex){console.log(ex);}") + private static native void playSafe(HTMLMediaElement el); + + + @JSBody(params={"el"}, script="if (!el) return ''; if (!el.error) return ''; try {return el.error.message;} catch (e) { return '';}") + private static native String getErrorMessage(HTMLMediaElement el); + + @JSBody(params={"el"}, script="if (!el) return 0; if (!el.error) return 0; try {return el.error.code;} catch (e) { return 0;}") + private static native int getErrorCode(HTMLMediaElement el); + + public PlayRequestInternal playAsyncInternal() { + return playAsync(new PlayRequestInternal()); + } + + + private class PlayRequestInternal extends AsyncResource { + private boolean initiatedByUserPrompt; + private boolean disallowed; + + @Override + public void complete(Media value) { + if (pendingPlayRequest == this) { + pendingPlayRequest = null; + } + super.complete(value); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void error(Throwable t) { + if (pendingPlayRequest == this) { + pendingPlayRequest = null; + } + super.error(t); + } + + + + + } + + /** + * Loads the media then plays it. This is called by playAsync() + * if it detects that the media isn't loaded yet. + * @param out + * @return + */ + private PlayRequestInternal loadThenPlay(final PlayRequestInternal out) { + // We're not in playable state yet. + // Let's trigger a load on this media, and + // wait for the state to change so that we can play + // Then we'll try to play again. + + class StateChangeListener implements ActionListener { + ActionListener onError; + @Override + public void actionPerformed(ActionEvent t) { + if (out.isDone()) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + return; + } + StateChangeEventInternal evt = (StateChangeEventInternal)t; + + + + if (evt.newState.canPlay) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + playAsync(out); + return; + } + } + + }; + final StateChangeListener onStateChange = new StateChangeListener(); + + ActionListener onError = new ActionListener() { + @Override + public void actionPerformed(MediaErrorEvent t) { + removeMediaErrorListener(this); + stateListeners.removeListener(onStateChange); + if (out.isDone()) { + return; + } + + out.error(t.getMediaException()); + + } + + }; + onStateChange.onError = onError; + stateListeners.addListener(onStateChange); + addMediaErrorListener(onError); + loadMedia(el); + return out; + } + + @Override + public String toString() { + if (el == null) { + return "HTML5Media(null)"; + } + String src = el.getSrc(); + return "HTML5Media("+src+")"; + } + + + + private PlayRequestInternal playAsyncForBrowsersThatReturnPromise(final PlayRequestInternal out) { + // This browser will return a promise from the media play() method + // so we can detect if play was disallowed. + if (out.isDone()) { + return out; + } + + class StateChangeListener implements ActionListener { + ActionListener onError; + @Override + public void actionPerformed(ActionEvent t) { + if (out.isDone()) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + return; + } + StateChangeEventInternal evt = (StateChangeEventInternal)t; + if (evt.newState.playing) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + out.complete(HTML5Media.this); + return; + } + } + + }; + final StateChangeListener onStateChange = new StateChangeListener(); + final ActionListener onError = new ActionListener() { + @Override + public void actionPerformed(MediaErrorEvent t) { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(this); + if (out.isDone()) { + return; + } + out.error(t.getMediaException()); + } + + }; + onStateChange.onError = onError; + stateListeners.addListener(onStateChange); + addMediaErrorListener(onError); + if (!out.disallowed) { + // This play request hasn't been disallowed yet + // So we'll just go ahead and try to play the media. + // If the browser blocks playback, as most modern browsers + // do if playing media without a user interaction + // then it will give us a not allowed error. + // In that case we'll respond by prompting the user to play + // the media. + final boolean wasUnlocked = isUnlocked(el); + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise: Attempt to play media id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallowed=false"); + playCatch(el, new NotAllowedHandler() { + @Override + public void onNotAllowed() { + new Thread(new Runnable() { + public void run() { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise: Play not allowed. id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallows=false"); + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + + if (out.isDone()) { + return; + } + if (out.initiatedByUserPrompt) { + // This was already initiated by a user prompt + // so the failure must have been for a reason that + // we can't remedy with a user prompt. + // In this case, just return the error + out.error(new MediaException(MediaErrorType.Aborted, "Play disallowed by the user or browser")); + } else { + // This wasn't initiated by a user prompt + // so we can give that a try. + out.disallowed = true; + CN.callSerially(new Runnable() { + public void run() { + playAsync(out); + } + }); + + } + + } + }).start(); + } + }, new FailedToPlayHandler() { + @Override + public void failedToPlay(final String msg) { + new Thread(new Runnable() { + public void run() { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise: Failed to play. id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallows=false"); + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + + if (out.isDone()) { + return; + } + out.error(new MediaException(MediaErrorType.Unknown, msg)); + + + } + }).start(); + } + }); + } else { + HTML5Implementation impl = HTML5Implementation.getInstance(); + if (impl.isBacksideHookAvailable()) { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Adding back-side hook for media. id="+getMediaID(el)+", unlocked="+isUnlocked(el)+", disallowed=true"); + impl.addBacksideHook(new HTML5Implementation.JSRunnable() { + @Override + public void run() { + // IMPORTANT: This is run on the Main thread so we can't do any fancy + // synchronous stuff. + final boolean wasUnlocked = isUnlocked(el); + + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Trying to play media inside back-side hook. id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallowed=true"); + playCatch(el, new NotAllowedHandler() { + @Override + public void onNotAllowed() { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Playing media disallowed inside back-side hook. id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallowed=true"); + new Thread(new Runnable() { + @Override + public void run() { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + + if (out.isDone()) { + return; + } + out.disallowed = true; + // Apparently we're not allowed to run this, even on the backside hook. + out.error(new MediaException(MediaErrorType.Aborted, "Media was blocked by the browser")); + + } + + }).start(); + } + }, new FailedToPlayHandler() { + @Override + public void failedToPlay(final String msg) { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Failed to play media inside back-side hook. msg="+msg+", id="+getMediaID(el)+", unlocked="+wasUnlocked+", disallowed=true"); + new Thread(new Runnable() { + @Override + public void run() { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + + if (out.isDone()) { + return; + } + out.error(new MediaException(MediaErrorType.Unknown, msg)); + } + + }); + } + }); + } + }); + } else { + // We've already been disallowed once, and there are no backside hooks available + // So we'll prompt the user to play the media, which should generate a backside + // hook that we can use. + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Prompting user to play media because it has been disallowed once, and there are no back-side hooks available. id="+getMediaID(el)+", unlocked="+isUnlocked(el)+", disallowed=true"); + promptUserToPlayMedia(new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:User confirmed playing media in prompt. id="+getMediaID(el)+", unlocked="+isUnlocked(el)+", disallowed=true"); + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + + return; + } + out.initiatedByUserPrompt = true; + playAsync(out); + } + + }, new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + _log("HTML5Media#playAsyncForBrowsersThatReturnPromise:Cancelled playing media at prompt. id="+getMediaID(el)+", unlocked="+isUnlocked(el)+", disallowed=true"); + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + return; + } + + out.error(new MediaException(MediaErrorType.Aborted, "Play was aborted by the user")); + } + + }); + } + } + return out; + } + + private PlayRequestInternal playAsyncForBrowsersThatDoNotReturnPromise(final PlayRequestInternal out) { + // This browser will return a promise from the media play() method + // so we can detect if play was disallowed. + if (out.isDone()) { + return out; + } + class StateChangeListener implements ActionListener { + ActionListener onError; + @Override + public void actionPerformed(ActionEvent t) { + if (out.isDone()) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + return; + } + StateChangeEventInternal evt = (StateChangeEventInternal)t; + + if (evt.newState.playing) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + out.complete(HTML5Media.this); + return; + } + } + + }; + final StateChangeListener onStateChange = new StateChangeListener(); + final ActionListener onError = new ActionListener() { + @Override + public void actionPerformed(MediaErrorEvent t) { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(this); + if (out.isDone()) { + return; + } + out.error(t.getMediaException()); + } + + }; + onStateChange.onError = onError; + + stateListeners.addListener(onStateChange); + addMediaErrorListener(onError); + playSafe(el); + return out; + } + + private PlayRequestInternal playAsync(final PlayRequestInternal out) { + if (out.isDone()) { + return out; + } + if (!CN.isEdt()) { + CN.callSerially(new Runnable() { + public void run() { + playAsync(out); + } + }); + return out; + } + if (el == null) { + out.error(new IllegalStateException("Attempt to playAsync() media that has already been cleaned up")); + return out; + } + if (pendingPauseRequest != null) { + pendingPauseRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + playAsync(out); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + playAsync(out); + } + } + }); + pendingPauseRequest = null; + pendingPlayRequest = out; + return out; + } + if (pendingPlayRequest != null && pendingPlayRequest != out) { + pendingPlayRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + out.complete(HTML5Media.this); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + if (currentState.playing && !pausePending) { + // It's already playing + // If there is a pause pending, then we'll flush this through + // to override the pending pause as this should hit after the pause. + out.complete(this); + return out; + } + pendingPlayRequest = out; + if (!currentState.canPlay) { + // the media isn't loaded enough to play yet. + // Let's start the load, and then play when the media can play. + return loadThenPlay(out); + } + + + if (playMethodReturnsPromise()) { + // Most newer browsers return a promise from play() so we're able to handle + // permissions errors more elegantly. Because the workflow is different + // we handle the two types of browsers separately. + return playAsyncForBrowsersThatReturnPromise(out); + } else { + return playAsyncForBrowsersThatDoNotReturnPromise(out); + } + + } + + private MessageEvent currPrompt; + + private void promptUserToPlayMedia(final ActionListener onPlay, final ActionListener onCancel) { + if (!CN.isEdt()) { + System.out.println("not on EDT in promptUserToPlayMedia: Redispatching on EDT"); + CN.callSerially(new Runnable() { + public void run() { + promptUserToPlayMedia(onPlay, onCancel); + } + }); + return; + } + // Let the outside webpage know that we are prompting the user for interaction + // so that it can display the iframe containing the app if deployed headlessly. + if (currPrompt != null && !currPrompt.getPromptPromise().isDone()) { + currPrompt.getPromptPromise().onResult(new AsyncResult() { + @Override + public void onReady(Boolean res, Throwable err) { + if (err == null && res) { + onPlay.actionPerformed(new ActionEvent(null)); + } else { + onCancel.actionPerformed(new ActionEvent(null)); + } + //Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + } + }); + return; + } + + Event evt = createJavascriptPromptEvent("MEDIA_READY"); + System.out.println("Prompting user to play media "+this); + //System.out.println("Request is done? "+out.isDone()); + Window.current().dispatchEvent(evt); + // Give the app developer a chance to create his own dialog + PromptPromise result = new PromptPromise(); + final MessageEvent promptEvent = new MessageEvent(result, "Media Ready", 426); + currPrompt = promptEvent; + result.onResult(new AsyncResult() { + @Override + public void onReady(Boolean res, Throwable err) { + if (err == null && res) { + onPlay.actionPerformed(promptEvent); + } else { + onCancel.actionPerformed(promptEvent); + } + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + } + }); + + + System.out.println("Dispatching prompt event. On EDT? "+CN.isEdt()); + Display.getInstance().dispatchMessage(promptEvent); + if (promptEvent.isConsumed()) { + // If the app consumed this message event, that means it is handling the prompt + // we can just return + System.out.println("Prompt event was consumed."); + return; + } + System.out.println("Prompt event was not consumed. Showing popup prompt"); + + currPrompt = null; + if (playMediaSheet != null) { + playMediaSheet.back(); + playMediaSheet = null; + } + final boolean[] playButtonPressed = new boolean[1]; + final Sheet sheet = new Sheet(Sheet.getCurrentSheet(), "Media Ready"); + Button playButton = new Button("Play Now"); + playButton.setMaterialIcon(FontImage.MATERIAL_PLAY_ARROW); + playButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + onPlay.actionPerformed(arg0); + playButtonPressed[0] = true; + sheet.back(); + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + } + + }); + sheet.addCloseListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + + if (!playButtonPressed[0]) { + onCancel.actionPerformed(t); + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + } + } + + }); + sheet.getContentPane().setLayout(BoxLayout.y()); + sheet.getContentPane().add(FlowLayout.encloseCenter(playButton)); + playMediaSheet = sheet; + sheet.show(); + } + + + @JSBody(params={"description"}, script="return new CustomEvent('cn1userprompt', {detail: description})") + private native static Event createJavascriptPromptEvent(String description); + + @JSBody(params={"response"}, script="return new CustomEvent('cn1userpromptresponse', {detail: response})") + private native static Event createJavascriptPromptCompleteEvent(boolean response); + + + @JSBody(params={"el"}, script="try{ el.load();} catch (e) {console.log(e);}") + private static native void loadMedia(HTMLMediaElement el); + + private class PauseRequestInternal extends AsyncResource { + + @Override + public void complete(Media value) { + if (pendingPauseRequest == this) { + pendingPauseRequest = null; + } + super.complete(value); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void error(Throwable t) { + if (pendingPauseRequest == this) { + pendingPauseRequest = null; + } + super.error(t); //To change body of generated methods, choose Tools | Templates. + } + + + + } + + private PauseRequestInternal pauseAsyncInternal() { + return pauseAsync(new PauseRequestInternal()); + } + + private PauseRequestInternal pauseAsync(final PauseRequestInternal out) { + if (!CN.isEdt()) { + CN.callSerially(new Runnable() { + public void run() { + pauseAsync(out); + } + }); + return out; + } + if (out.isDone()) { + return out; + } + if (el == null) { + out.error(new IllegalStateException("Media has already been cleaned up")); + return out; + } + if (pendingPlayRequest != null) { + pendingPlayRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + pauseAsync(out); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + pauseAsync(out); + } + } + }); + pendingPlayRequest = null; + pendingPauseRequest = out; + return out; + } + if (pendingPauseRequest != null && pendingPauseRequest != out) { + pendingPauseRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + out.complete(t); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + pendingPauseRequest = out; + if (currentState.paused) { + // It's already playing + // If there is a pause pending, then we'll flush this through + // to override the pending pause as this should hit after the pause. + out.complete(this); + return out; + } + + final ActionListener onStateChange = new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + if (out.isDone()) { + stateListeners.removeListener(this); + return; + } + StateChangeEventInternal evt = (StateChangeEventInternal)t; + + if (evt.newState.paused) { + stateListeners.removeListener(this); + out.complete(HTML5Media.this); + return; + } + } + + }; + stateListeners.addListener(onStateChange); + //el.pause(); + pauseNative(el); + return out; + } + + @Override + protected void pauseImpl() { + throw new RuntimeException("Shouldn't need this because we override pauseAsync()"); + } + + @Override + protected void playImpl() { + throw new RuntimeException("Shouldn't need this because we override playAsync()"); + } + + @JSBody(params={"el"}, script="if (window.cn1DebugPauseFunction) window.cn1DebugPauseFunction(el); else el.pause();") + private static native void pauseNative(HTMLMediaElement el); + + + @Override + public PauseRequest pauseAsync() { + + final PauseRequest out = new PauseRequest(); + if (!CN.isEdt()) { + Log.e(new IllegalStateException("WARNING: Calling Media.pauseAsync off the EDT")); + + } + pauseAsyncInternal().ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (el == null) { + out.error(new IllegalStateException("Attempt to pause media that is already cleaned up")); + return; + } + out.complete(HTML5Media.this); + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + out.error(t); + } + }); + return out; + } + + @Override + public PlayRequest playAsync() { + if (!CN.isEdt()) { + Log.e(new IllegalStateException("WARNING: Calling Media.playAsync off the EDT")); + } + final PlayRequest out = new PlayRequest(); + if (el == null) { + out.error(new IllegalStateException("Attempt to play media that is already cleaned up.")); + return out; + } + playAsyncInternal().ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + if (el == null) { + out.error(new IllegalStateException("Attempt to play media that is already cleaned up")); + return; + } + out.complete(HTML5Media.this); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + + + + @Override + public int getTime() { + if (el == null || currentState == null || !currentState.canPlay) { + return -1; + } + + return (int)(el.getCurrentTime() * 1000); + } + + @Override + public int getDuration() { + if (el == null || currentState == null || !currentState.canPlay) { + return -1; + } + + return (int)(el.getDuration() * 1000); + } + + @Override + public boolean isPlaying() { + if (el == null) { + return false; + } + + return currentState.playing; + } + + @Override + public int getVolume() { + if (el == null) { + return 0; + } + + return (int)(el.getVolume()*100.0); + } + + private boolean shouldSetMuteOnZeroVolume() { + return HTML5Implementation.isIOS() && "true".equals(CN.getProperty("javascript.iosMuteOnZeroVolume", "true")); + } + + @Override + public void setVolume(int volume) { + if (el == null) { + return; + } + if (volume == 0 && shouldSetMuteOnZeroVolume()) { + el.setMuted(true); + } else if (shouldSetMuteOnZeroVolume()) { + el.setMuted(false); + } + el.setVolume(volume/100.0); + } + + @Override + public void setTime(int time) { + pendingTime = time; + if (el == null) { + return; + } + el.setCurrentTime(time/1000.0); + waitWhileSeeking(5000); + + } + + @Override + public void setVariable(String string, Object o) { + if (el == null) { + return; + } + if (Media.VARIABLE_NATIVE_CONTRLOLS_EMBEDDED.equals(string) && o instanceof Boolean) { + el.setControls((Boolean)o); + } + } + + @Override + public Object getVariable(String string) { + return null; + } + + + @Override + public void prepare() { + if (el != null) { + loadMedia(el); + } + } + + @Override + public void cleanup() { + _log("In Media.cleanup"); + + if (el != null) { + //el.pause(); + pauseNative(el); + HTMLMediaElement tmp = el; + el = null; + HTML5Implementation.getInstance().mediaPool().returnMediaElement(tmp); + } + } + + @Override + public void setNativePlayerMode(boolean bln) { + + } + + @Override + public boolean isNativePlayerMode() { + return false; + } + + @Override + public boolean isFullScreen() { + if (el == null) { + return false; + } + if (isVideo) { + return el.hasAttribute("playsinline"); + //return ((HTMLVideoElement)el).isDisplayingFullscreen(); + } + return false; + } + + @Override + public void setFullScreen(boolean full) { + if (el == null) { + return; + } + if (isVideo) { + HTMLVideoElement videoEl = (HTMLVideoElement)el; + + if (full){ + videoEl.removeAttribute("playsinline"); + videoEl.enterFullscreen(); + } else { + videoEl.setAttribute("playsinline", ""); + videoEl.exitFullscreen(); + } + } + } + + @Override + public boolean isVideo() { + return isVideo; + } + + + public class MediaComponent extends HTML5Peer { + + public MediaComponent() { + super(HTML5Media.this.el); + } + + + @Override + protected void initComponent() { + super.initComponent(); + //el.getOwnerDocument().getBody().appendChild(el); + + } + + @Override + protected void deinitialize() { + super.deinitialize(); + //el.getOwnerDocument().getBody().removeChild(el); + } + } + + private boolean isSeeking() { + if (HTML5Media.this.el == null) return false; + return HTML5Media.this.el.isSeeking(); + } + + private void waitWhileSeeking(final long timeout) { + if (!isSeeking()) return; + final Runnable waitRunnable = new Runnable() { + public void run() { + long timeOutTime = System.currentTimeMillis() + timeout; + while (timeOutTime > System.currentTimeMillis() && isSeeking()) { + Util.sleep(50); + } + } + }; + if (CN.isEdt()) { + CN.invokeAndBlock(waitRunnable); + } else { + waitRunnable.run(); + } + } + + + private static boolean isUnlocked(HTMLMediaElement el) { + return HTML5MediaPool.isUnlocked(el); + } + + private static String getMediaID(HTMLMediaElement el) { + return HTML5MediaPool.getMediaID(el); + } +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaPool.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaPool.java new file mode 100644 index 0000000000..088acfbe29 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaPool.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import static com.codename1.impl.html5.HTML5Implementation._log; +import com.codename1.impl.html5.JSOImplementations.HTMLAudioElement; +import com.codename1.impl.html5.JSOImplementations.HTMLMediaElement; +import java.util.ArrayList; +import org.teavm.jso.JSBody; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.html.HTMLElement; + +/** + * + * @author shannah + */ +public class HTML5MediaPool { + private static int nextIndex=1; + private final Window window = Window.current(); + private int maxSize=3; + private final ArrayList videoPool = new ArrayList<>(); + private final ArrayList audioPool = new ArrayList<>(); + private final ArrayList cleanupListeners = new ArrayList<>(); + public static abstract class CleanupListener { + private final HTMLElement el; + + public CleanupListener(HTMLElement el) { + this.el = el; + } + + + + public abstract void run(HTMLElement el); + } + + public void addCleanupListener(CleanupListener r) { + cleanupListeners.add(r); + } + + + + public JSOImplementations.HTMLVideoElement createVideoElement() { + if (videoPool.isEmpty()) { + return (JSOImplementations.HTMLVideoElement)window.getDocument().createElement("video"); + } else { + return videoPool.remove(0); + } + } + + @JSBody(params={}, script="if (window._unlockedAudioPool){ " + + "var el = window._unlockedAudioPool.pop(); " + + "if (el) return el; " + + "else return null;" + + "} else { return null;}") + private static native JSOImplementations.HTMLAudioElement getAudioElementFromNativePool(); + + public JSOImplementations.HTMLAudioElement createAudioElement() { + _log("HTML5MediaPool#createAudioElement"); + if (audioPool.isEmpty()) { + _log("Pool is empty. Checking native pool"); + JSOImplementations.HTMLAudioElement out = getAudioElementFromNativePool(); + + if (out != null) { + if (out.getAttribute("cn1-audio-id") == null) { + out.setAttribute("cn1-audio-id", ""+(nextIndex++)); + } + _log("Returning audio element from native pool with audio ID "+out.getAttribute("cn1-audio-id")); + return out; + } + out = (JSOImplementations.HTMLAudioElement)window.getDocument().createElement("audio"); + out.setAttribute("cn1-audio-id", ""+(nextIndex++)); + _log("No audio element found in native pool. Audio Element created with ID: "+out.getAttribute("cn1-audio-id")); + return out; + } else { + + JSOImplementations.HTMLAudioElement out = audioPool.remove(0); + _log("Creating audio element from pool with ID "+out.getAttribute("cn1-audio-id")); + return out; + } + } + + private void cleanup(HTMLElement el) { + ArrayList tmp = new ArrayList(); + for (CleanupListener l : cleanupListeners) { + if (l.el == el) { + tmp.add(l); + l.run(el); + } + } + cleanupListeners.removeAll(tmp); + } + + + public static boolean isUnlocked(HTMLMediaElement el) { + return "true".equals(el.getAttribute("data-cn1-unlocked")); + } + + + public static void markUnlocked(HTMLMediaElement el) { + el.setAttribute("data-cn1-unlocked", "true"); + + } + + public static void markLocked(HTMLMediaElement el) { + el.removeAttribute("data-cn1-unlocked"); + } + + private void returnAudioElement(JSOImplementations.HTMLAudioElement el) { + cleanup(el); + _log("HTML5MediaPool#returnAudioElement "+el.getAttribute("cn1-audio-id")); + if (audioPool.size() < maxSize && isUnlocked(el)) { + + audioPool.add(el); + _log("Audio element with ID "+el.getAttribute("cn1-audio-id")+" returned to pool. Pool size now "+audioPool.size()); + } else { + _log("Audio element with ID "+el.getAttribute("cn1-audio-id")+" not returned to pool. Unlocked="+isUnlocked(el)+", pool size="+audioPool.size()); + } + } + + private void returnVideoElement(JSOImplementations.HTMLVideoElement el) { + cleanup(el); + if (videoPool.size() < maxSize && isUnlocked(el)) { + videoPool.add(el); + } + } + + @JSBody(params={"el"}, script="return el.tagName") + private native static String getTagName(HTMLElement el); + + public void returnMediaElement(JSOImplementations.HTMLMediaElement el) { + if (getTagName(el).toLowerCase().equals("video")) { + returnVideoElement((JSOImplementations.HTMLVideoElement)el); + } else if (getTagName(el).toLowerCase().equals("audio")) { + returnAudioElement((JSOImplementations.HTMLAudioElement)el); + } else { + _log("Failed to return media element to pool because tag name unsupported: "+getTagName(el)); + } + } + + public static String getMediaID(HTMLMediaElement el) { + if (getTagName(el).toLowerCase().equals("video")) { + return ""; + } else { + String out = el.getAttribute("cn1-audio-id"); + if (out == null) out = ""; + return out; + } + } +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaRecorder.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaRecorder.java new file mode 100644 index 0000000000..46571fddf4 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5MediaRecorder.java @@ -0,0 +1,1086 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.impl.html5.HTML5Implementation.JSRunnable; +import static com.codename1.impl.html5.HTML5Implementation._log; +import com.codename1.impl.html5.HTML5Media.NotAllowedHandler; +import com.codename1.io.Log; +import com.codename1.io.Util; +import com.codename1.media.AbstractMedia; +import com.codename1.media.AudioBuffer; +import com.codename1.media.Media; +import com.codename1.media.MediaManager; +import com.codename1.media.MediaRecorderBuilder; +import com.codename1.ui.Button; +import com.codename1.ui.CN; +import static com.codename1.ui.CN.invokeAndBlock; +import com.codename1.ui.Component; +import com.codename1.ui.Display; +import com.codename1.ui.FontImage; +import com.codename1.ui.Sheet; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.MessageEvent; +import com.codename1.ui.events.MessageEvent.PromptPromise; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.FlowLayout; +import com.codename1.ui.util.EventDispatcher; +import com.codename1.util.AsyncResource; +import com.codename1.util.AsyncResult; +import com.codename1.util.EasyThread; +import com.codename1.util.SuccessCallback; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.typedarrays.Float32Array; + +/** + * + * @author shannah + */ +public class HTML5MediaRecorder extends AbstractMedia { + + private CN1AudioRecorder peer; + //private boolean pauseFlag; + private float[] pcmBuffer; + private StateInternal currentState = new StateInternal(); + //private Set pendingRecordRequests = new HashSet(); + private RecordRequest pendingRecordRequest; + private PauseRequestInternal pendingPauseRequest; + private final Object onCompleteLock = new Object(); + private boolean waitingForComplete = false; + private AudioBuffer audioBuffer; + private EasyThread processingThread; + private Sheet playMediaSheet; + private final EventDispatcher stateListeners = new EventDispatcher(); + + private static class StateInternal { + private boolean recording, paused; + + StateInternal(boolean recording, boolean paused) { + this.recording = recording; + this.paused = paused; + } + + StateInternal() { + paused = true; + } + + StateInternal(StateInternal state) { + this.recording = state.recording; + this.paused = state.paused; + } + + @Override + public String toString() { + return "State{recording:"+recording+", paused:"+paused+"}"; + } + + + } + + + private static class StateChangeEvent extends ActionEvent { + private StateInternal oldState, newState; + + StateChangeEvent(HTML5MediaRecorder source, StateInternal oldState, StateInternal newState) { + super(source); + this.oldState = oldState; + this.newState = newState; + } + } + + private void setState(StateInternal newState) { + StateInternal oldState = new StateInternal(currentState); + currentState = new StateInternal(newState); + System.out.println("HTML5MediaRecorder setState("+newState+")"); + stateListeners.fireActionEvent(new StateChangeEvent(this, oldState, new StateInternal(newState))); + if (newState.recording && !oldState.recording) { + fireMediaStateChange(State.Playing); + } else if (newState.paused && !oldState.paused) { + fireMediaStateChange(State.Paused); + } + } + + + @JSFunctor + private interface StringCallback extends JSObject { + public void callback(String arg); + } + + public HTML5MediaRecorder(MediaRecorderBuilder builder) { + if (builder.isRedirectToAudioBuffer()) { + audioBuffer = MediaManager.getAudioBuffer(builder.getPath(), true, 256); + pcmBuffer = new float[audioBuffer.getMaxSize()]; + final float[] fPcmBuffer = pcmBuffer; + processingThread = EasyThread.start("AudioBufferProcessor"); + final EasyThread fProcessingThread = processingThread; + final AudioBuffer fAudioBuffer = audioBuffer; + peer = createAudioUnit(builder.getSamplingRate(), 16, builder.getAudioChannels(), new CN1AudioProcessor() { + @Override + public void onAudioProcess(final int sampleRate, final int numChannels, final Float32Array data) { + + new Thread(new Runnable() { + public void run() { + + fProcessingThread.run(new Runnable() { + public void run() { + + int len = data.getLength(); + int sampleBufferPos = 0; + int audioBufferLen = fAudioBuffer.getMaxSize(); + for (int i= 0; i < len; i++) { + fPcmBuffer[sampleBufferPos] = data.get(i); + sampleBufferPos++; + if (sampleBufferPos >= audioBufferLen) { + fAudioBuffer.copyFrom(sampleRate, numChannels, fPcmBuffer, 0, sampleBufferPos); + sampleBufferPos = 0; + } + } + + if (sampleBufferPos > 0) { + fAudioBuffer.copyFrom(sampleRate, numChannels, fPcmBuffer, 0, sampleBufferPos); + sampleBufferPos = 0; + } + + + } + }); + } + + + + }).start(); + + + + } + }, new StringCallback() { + @Override + public void callback(final String arg) { + new Thread(new Runnable() { + public void run() { + if (!currentState.paused) { + + StateInternal newState = new StateInternal(currentState); + newState.paused = true; + newState.recording = false; + System.out.println("HTML5MediaRecorder setting state in onComplete callback"); + setState(newState); + } + synchronized(onCompleteLock) { + waitingForComplete = false; + onCompleteLock.notifyAll(); + } + } + + + }).start(); + } + + }, + new StringCallback() { + @Override + public void callback(final String arg) { + new Thread(new Runnable() { + public void run() { + fireMediaError(new MediaException(MediaErrorType.LineUnavailable, arg)); + } + }).start(); + + } + + }, + new OnRecord() { + @Override + public void recorderStarted(final int numChannels, final int sampleRate) { + new Thread(new Runnable() { + @Override + public void run() { + + CN.callSerially(new Runnable() { + public void run() { + if (!currentState.recording) { + System.out.println("HTML5MediaRecorder setting state in onRecord callback"); + StateInternal newState = new StateInternal(currentState); + newState.recording = true; + newState.paused = false; + setState(newState); + } + + } + }); + + + } + + }).start(); + } + + }); + } else { + peer = createAudioRecorder(builder.getPath(), + new StringCallback() { + @Override + public void callback(final String arg) { + new Thread(new Runnable() { + public void run() { + if (currentState.recording) { + StateInternal newState = new StateInternal(currentState); + newState.recording = false; + newState.paused = true; + setState(newState); + } + synchronized(onCompleteLock) { + waitingForComplete = false; + onCompleteLock.notifyAll(); + } + } + + }).start(); + } + + }, + new StringCallback() { + @Override + public void callback(final String arg) { + new Thread(new Runnable() { + public void run() { + fireMediaError(new MediaException(MediaErrorType.LineUnavailable, arg)); + } + }).start(); + + } + + }, + new OnRecord() { + @Override + public void recorderStarted(int numChannels, int sampleRate) { + CN.callSerially(new Runnable() { + public void run() { + if (!currentState.recording) { + StateInternal newState = new StateInternal(currentState); + newState.recording = true; + newState.paused = false; + setState(newState); + } + } + }); + } + } + ); + } + } + + + @JSBody(params={"path", "onComplete", "onError", "onRecord"}, script="return new CN1AudioRecorder({onComplete:onComplete, savePath:path, onError:onError, onRecord:onRecord});") + private native static CN1AudioRecorder createAudioRecorder(String path, StringCallback onComplete, StringCallback onError, OnRecord onRecord); + + @JSBody(params={"sampleRate", "sampleSize", "audioChannels", "onAudioProcess", "onComplete", "onError", "onRecord"}, script="return new CN1AudioUnit({sampleRate:sampleRate, sampleSize:sampleSize, audioChannels:audioChannels, onAudioProcess:onAudioProcess, onComplete:onComplete, onError:onError, onRecord:onRecord});") + private native static CN1AudioRecorder createAudioUnit(int sampleRate, int sampleSize, int audioChannels, CN1AudioProcessor onAudioProcess, StringCallback onComplete, StringCallback onError, OnRecord onRecord); + + @JSBody(params={}, script="if (window.recordRequestComplete){window.recordRequestComplete();}") + private native static void fireRecordRequestComplete(); + + private class RecordRequest extends AsyncResource { + private boolean disallowed; + private boolean promptedByUser; + private boolean canceled; + + @Override + public void complete(Media value) { + HTML5Implementation._log("Record request complete"); + fireRecordRequestComplete(); + if (pendingRecordRequest == this) { + pendingRecordRequest = null; + } + super.complete(value); + } + + @Override + public void error(Throwable t) { + if (pendingRecordRequest == this) { + pendingRecordRequest = null; + } + fireRecordRequestComplete(); + HTML5Implementation._log("Record request complete with error "); + super.error(t); + } + + } + + private class PauseRequestInternal extends AsyncResource { + + @Override + public void complete(Media value) { + if (pendingPauseRequest == this) { + pendingPauseRequest = null; + } + super.complete(value); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void error(Throwable t) { + if (pendingPauseRequest == this) { + pendingPauseRequest = null; + } + super.error(t); //To change body of generated methods, choose Tools | Templates. + } + + + + } + + + + + public RecordRequest playAsyncInternal() { + return playAsync(new RecordRequest()); + } + + @Override + public PlayRequest playAsync() { + if (!CN.isEdt()) { + Log.e(new IllegalArgumentException("WARNING: Calling MediaRecorder.playAsync() off the EDT.")); + } + final PlayRequest out = new PlayRequest(); + playAsyncInternal().ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + out.complete(HTML5MediaRecorder.this); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + + private MessageEvent currPrompt; + + public RecordRequest playAsync(final RecordRequest out) { + if (!CN.isEdt()) { + CN.callSerially(new Runnable() { + public void run() { + playAsync(out); + } + }); + return out; + } + System.out.println("HTML5MediaRecorder.playAsync "+currentState); + if (out.isDone()) { + // The request was already completed. + // We don't need to proceed any further. + return out; + } + + if (pendingPauseRequest != null) { + // There is a pending pause request + // We need to attach to that pause request and play after the pause is complete + System.out.println("HTML5MediaRecorder.playAsync - pendingPauseRequest.ready()"); + pendingPauseRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + System.out.println("HTML5MEdiaRecorder.playAsync - Pause resolved onSuccess. Now what? "+out.isDone()); + if (!out.isDone()) { + + playAsync(out); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + System.out.println("HTML5MEdiaRecorder.playAsync - Pause resolved onError. Now what? "+out.isDone()); + if (!out.isDone()) { + playAsync(out); + } + } + }); + // As far as anyone that comes after is concerned, there + // is no longer a pending pause request. There is a pending + // record request - *this* request. + pendingPauseRequest = null; + pendingRecordRequest = out; + return out; + } + + if (pendingRecordRequest != null && pendingRecordRequest != out) { + // There is a pending record request. We need to wait until that request + // is complete. + System.out.println("HTML5MediaRecorder.playAsync - pendingRecordRequest.ready()"); + pendingRecordRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (out.isDone()) { + return; + } + out.complete(t); + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (out.isDone()) { + return; + } + out.error(t); + } + }); + return out; + } + + // If we are here then there are no pending requests. + // Mark ourself as the pending request. + + pendingRecordRequest = out; + if (currentState.recording) { + // We were already recording. + // Do nothing and just complete the request. + System.out.println("HTML5Media.playAsync - already recording"); + out.complete(this); + return out; + } + + // If we are here, then there are no pending requests AND we are not + // currently recording. We should be clear to record. + + // We install a change listener to listen for errors or a change of state + // to recording. + // If either of these things happen, then we know that the "play" took effect. + class StateChangeListener implements ActionListener { + ActionListener onError; + @Override + public void actionPerformed(ActionEvent t) { + StateChangeEvent evt = (StateChangeEvent)t; + + if (out.isDone()) { + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + return; + } + + if (evt.newState.recording) { + // The state of this media has changed to recording + // We can remove the state listener we added + // (and error listener), and complete the promise + stateListeners.removeListener(this); + if (onError != null) { + removeMediaErrorListener(onError); + } + System.out.println("HTML5MediaRecorder record seemed to work. Current state "+currentState); + out.complete(HTML5MediaRecorder.this); + return; + } + + + } + + }; + final StateChangeListener onStateChange = new StateChangeListener(); + + final ActionListener onError = new ActionListener() { + @Override + public void actionPerformed(MediaErrorEvent t) { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(this); + if (!out.isDone()) { + System.out.println("HTML5MediaRecorder.playAsync - media error: "+t.getMediaException().getMessage()); + out.error(t.getMediaException()); + } + } + + }; + onStateChange.onError = onError; + stateListeners.addListener(onStateChange); + addMediaErrorListener(onError); + + if (!out.disallowed) { + System.out.println("HTML5MediaRecorder playAsync !out.disallowed"); + // We haven't been denied access yet, so we should at least try. + peer.record(new NotAllowedHandler() { + @Override + public void onNotAllowed() { + // This callback is on the JS main thread. We need to wrap + // it in a Java thread then run it on the EDT + HTML5Implementation.callSerially(new Runnable() { + public void run() { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + return; + } + out.disallowed = true; + playAsync(out); + + } + }); + } + + }); + return out; + } + + // If we are here, then we have already attempted recording once and + // been denied by the browser. Probably access was denied because + // we weren't running in direct response to user interaction. + // We have some options remaining. The JS port installs "back-side" hooks + // whenever the pointer is pressed, which are basically setTimeout() calls + // which will run a callback delayed by a few hundred milliseconds. + // First we check if there are any back-side hooks available to latch onto. + HTML5Implementation impl = HTML5Implementation.getInstance(); + if (impl.isBacksideHookAvailable()) { + // As luck would have it, there is a back-side hook available. + // A back-side hook is installed every time the user presses or releases the + // pointer, and they hang around for long enough to react to the press + // inside a back-side hook. + System.out.println("HTML5MediaRecorder playAsync impl.backsidehookAvailable"); + impl.addBacksideHook(new JSRunnable() { + @Override + public void run() { + HTML5Implementation._log("Running backside hook"); + + if (out.isDone()) { + HTML5Implementation._log("Record request is already done."); + // The request was already completed somehow. + // Remember to remove the state and error listeners + // and back away quietly. + new Thread(new Runnable() { + public void run() { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + } + }).start(); + return; + } + + // Now that we are on the main thread we can try to issue + // a record again and it *should* work. + peer.record(new NotAllowedHandler() { + @Override + public void onNotAllowed() { + // OKay we got denied AGAIN!!! + // Let's just remove our state listeners + // return an error and throw our hands up in the air. + // Alas, we tried :( + HTML5Implementation.callSerially(new Runnable() { + public void run() { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + return; + } + MediaException ex = new MediaException(MediaErrorType.Aborted, "Media recording disallowed by browser permissions."); + out.error(ex); + fireMediaError(ex); + } + + }); + } + }); + + // NOTE: We don't need to add any more code here to cover the "Success" case + // because the state listener should be fired upon a successful record. + } + + }); + return out; + } else { + System.out.println("No back-side hook available"); + } + + // For the remainder of this, we really need to be on the EDT. + if (!CN.isEdt()) { + HTML5Implementation.callSerially(new Runnable() { + public void run() { + playAsync(out); + } + }); + return out; + } + + //If we are here then there were no back-side hooks available to latch onto. + // We will try to manufacture a new back-side hook by compelling the user to + // press somewhere on the screen. We do this by prompting the user. + if (out.promptedByUser) { + // We've already prompted the user, + // and there were no backside cache options last time + // so let's just report an error + System.out.println("HTML5MediaRecorder playAsync out.promptedByUser"); + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + + out.error(new MediaException(MediaErrorType.Aborted, "Recording disallowed by browser.")); + return out; + } + // Let the outside webpage know that we are prompting the user for interaction + // so that it can display the iframe containing the app if deployed headlessly. + if (currPrompt != null && !currPrompt.getPromptPromise().isDone()) { + currPrompt.getPromptPromise().onResult(new AsyncResult() { + @Override + public void onReady(final Boolean res, final Throwable err) { + + // NOTE: We don't need to dispatch prompt complete events from this callback + // becuse they would have been dispatched in the "currPrompt" that we are piggy-backing + // onto. + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + + //Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + return; + } + if (err == null && res) { + out.promptedByUser = true; + //Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + playAsync(out); + } else { + //Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + out.error(new MediaException(MediaErrorType.Aborted, "Record canceled by the user")); + } + } + + }); + return out; + } + + // Let the outside webpage know that we are prompting the user for interaction + // so that it can display the iframe containing the app if deployed headlessly. + Window.current().dispatchEvent(createJavascriptPromptEvent("AUDIO_RECORDER_READY")); + // Give the app developer a chance to create his own dialog + PromptPromise result = new PromptPromise(); + final MessageEvent promptEvent = new MessageEvent(result, "Audio Recorder Ready", 427); + currPrompt = promptEvent; + result.onResult(new AsyncResult() { + @Override + public void onReady(final Boolean res, final Throwable err) { + if (!CN.isEdt()) { + CN.callSerially(new Runnable() { + public void run() { + onReady(res, err); + + } + }); + //onReady(res, err); + return; + } + if (currPrompt == promptEvent) { + currPrompt = null; + } + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + return; + } + if (err == null && res) { + out.promptedByUser = true; + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + playAsync(out); + } else { + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + out.error(new MediaException(MediaErrorType.Aborted, "Record canceled by the user")); + } + } + }); + + + + Display.getInstance().dispatchMessage(promptEvent); + if (promptEvent.isConsumed()) { + // If the app consumed this message event, that means it is handling the prompt + // we can just return + System.out.println("Prompt event was consumed"); + return out; + } + + currPrompt = null; + + /* + if (!Window.confirm("This application would like to access your microphone")) { + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + out.error(new MediaException(MediaErrorType.Aborted, "Record canceled by the user")); + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + return out; + } + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + out.promptedByUser = true; + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + playAsync(out); + */ + //if (true) return out; + + System.out.println("About to show sheet"); + if (playMediaSheet != null) { + playMediaSheet.back(); + playMediaSheet = null; + } + Sheet currSheet = Sheet.getCurrentSheet(); + + final Sheet sheet = new Sheet(currSheet, "Audio Recorder Ready"); + final boolean[] recordPressed = new boolean[1]; + Button playButton = new Button("Start Recording"); + playButton.setMaterialIcon(FontImage.MATERIAL_RECORD_VOICE_OVER); + playButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + if (playMediaSheet == sheet) { + playMediaSheet = null; + } + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + recordPressed[0] = true; + if (out.isDone()) { + sheet.back(); + return; + } + out.promptedByUser = true; + playAsync(out); + + + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(true)); + sheet.back(); + } + + }); + sheet.addCloseListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent t) { + if (playMediaSheet == sheet) { + playMediaSheet = null; + } + stateListeners.removeListener(onStateChange); + removeMediaErrorListener(onError); + if (out.isDone()) { + + if (!recordPressed[0]) { + sheet.back(); + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + } + return; + } + if (!recordPressed[0]) { + out.error(new MediaException(MediaErrorType.Aborted, "Record canceled by the user")); + Window.current().dispatchEvent(createJavascriptPromptCompleteEvent(false)); + } + } + + }); + sheet.getContentPane().setLayout(BoxLayout.y()); + sheet.getContentPane().add(FlowLayout.encloseCenter(playButton)); + playMediaSheet = sheet; + + sheet.show(); + return out; + } + + @JSBody(params={"description"}, script="return new CustomEvent('cn1userprompt', {detail: description})") + private native static Event createJavascriptPromptEvent(String description); + + @JSBody(params={"response"}, script="return new CustomEvent('cn1userpromptresponse', {detail: response})") + private native static Event createJavascriptPromptCompleteEvent(boolean response); + + + @Override + protected void pauseImpl() { + throw new RuntimeException("Shouldn't need to implements pauseImpl because we override pauseAsync"); + } + + @Override + protected void playImpl() { + throw new RuntimeException("Shouldn't need to implement playImpl because we overrid playAsync"); + } + + + + + + + @Override + public PauseRequest pauseAsync() { + final PauseRequest out = new PauseRequest(); + if (!CN.isEdt()) { + Log.e(new IllegalArgumentException("WARNING: Calling MediaRecorder.pauseAsync() off the EDT")); + } + pauseAsyncInternal().ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + out.complete(HTML5MediaRecorder.this); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + + + + private PauseRequestInternal pauseAsyncInternal() { + return pauseAsync(new PauseRequestInternal()); + } + + private PauseRequestInternal pauseAsync(final PauseRequestInternal out) { + if (!CN.isEdt()) { + CN.callSerially(new Runnable() { + public void run() { + pauseAsync(out); + } + }); + return out; + } + if (out.isDone()) { + return out; + } + if (pendingRecordRequest != null) { + // There is a pending record request. We'll wait for that request + // to complete, then we'll immediately issue a pause. + pendingRecordRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + pauseAsync(out); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + pauseAsync(out); + } + } + }); + + // As far as we're concerned, the current pending request is now *this* + // request. Remove the pending record request, and replace it with this one. + pendingRecordRequest = null; + pendingPauseRequest = out; + return out; + } + + // If we are here, then there is no pending record request. + + if (pendingPauseRequest != null && pendingPauseRequest != out) { + // There is another pending pause request. + pendingPauseRequest.ready(new SuccessCallback() { + @Override + public void onSucess(Media t) { + if (!out.isDone()) { + out.complete(t); + } + } + }).except(new SuccessCallback() { + @Override + public void onSucess(Throwable t) { + if (!out.isDone()) { + out.error(t); + } + } + }); + return out; + } + + if (currentState.paused) { + out.complete(this); + return out; + } + + + // If we are here, then there is no existing pending pause request or record request + // Set ourself as the pending pause request. + pendingPauseRequest = out; + if (peer != null) { + // Issue the pause() call to the peer. + peer.pause(); + } + Timer t = new Timer(); + + t.schedule(new TimerTask(){ + public void run() { + CN.callSerially(new Runnable() { + public void run() { + StateInternal newState = new StateInternal(currentState); + newState.paused = true; + newState.recording = false; + setState(newState); + out.complete(HTML5MediaRecorder.this); + } + + }); + + } + }, getPauseDelay()); + + return out; + + } + + @JSBody(params={}, script="return window.cn1HTML5MediaRecorderPauseDelay || 1") + private static native int getPauseDelay(); + + @Override + public void prepare() { + + } + + @Override + public void cleanup() { + + if (peer == null) { + return; + } + pause(); + + peer.stop(); + waitingForComplete = true; + invokeAndBlock(new Runnable() { + @Override + public void run() { + while (waitingForComplete) { + synchronized(onCompleteLock) { + Util.wait(onCompleteLock); + } + } + } + + }); + if (processingThread != null) { + processingThread.run(new Runnable() { + public void run() { + if (processingThread != null) { + processingThread.kill(); + processingThread = null; + } + } + }); + } + peer = null; + + } + + protected void finalize() { + if(peer != null) { + cleanup(); + } + } + + @Override + public int getTime() { + return 0; + } + + @Override + public void setTime(int i) { + + } + + @Override + public int getDuration() { + return 0; + } + + @Override + public void setVolume(int i) { + + } + + @Override + public int getVolume() { + return 0; + } + + @Override + public boolean isPlaying() { + + return currentState.recording; + } + + @Override + public Component getVideoComponent() { + return null; + } + + @Override + public boolean isVideo() { + return false; + } + + @Override + public boolean isFullScreen() { + return false; + } + + @Override + public void setFullScreen(boolean bln) { + + } + + @Override + public void setNativePlayerMode(boolean bln) { + + } + + @Override + public boolean isNativePlayerMode() { + return false; + } + + @Override + public void setVariable(String string, Object o) { + + } + + @Override + public Object getVariable(String string) { + return null; + } + + @JSFunctor + public static interface OnRecord extends JSObject { + public void recorderStarted(int numChannels, int sampleRate); + } + + public interface CN1AudioRecorder extends JSObject { + public void record(HTML5Media.NotAllowedHandler notAllowedCallback); + public void pause(); + public void stop(); + public void resume(); + public boolean isRecording(); + } + + + + @JSFunctor + public interface CN1AudioProcessor extends JSObject { + public void onAudioProcess(int sampleRate, int numChannels, org.teavm.jso.typedarrays.Float32Array data); + } + + +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Peer.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Peer.java new file mode 100644 index 0000000000..fab424b0a3 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Peer.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.charts.util.ColorUtil; +import com.codename1.impl.html5.HTML5Implementation.NativeFont; +import com.codename1.teavm.jso.util.JS; +import com.codename1.ui.PeerComponent; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.css.CSSStyleDeclaration; +import org.teavm.jso.dom.html.HTMLElement; +import static com.codename1.impl.html5.HTML5Implementation.scaleCoord; +import static com.codename1.impl.html5.HTML5Implementation.unscaleCoord; +import com.codename1.ui.CN; +import com.codename1.ui.Component; +import com.codename1.ui.Font; +import com.codename1.ui.Graphics; +import com.codename1.ui.events.StyleListener; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.plaf.Border; +import com.codename1.ui.plaf.RoundRectBorder; +import com.codename1.ui.plaf.Style; +import org.teavm.jso.JSBody; +import org.teavm.jso.dom.html.HTMLOptionElement; +import org.teavm.jso.dom.xml.NodeList; +import org.w3c.dom.html.HTMLStyleElement; + + +/** + * + * @author shannah + */ +public class HTML5Peer extends PeerComponent { + + //HTMLElement el; + + // A flag that is set if the CN1 styles should be propagated + // to this element. This is handy for peer text fields and selects + // that you want to be able to style using CN1 standard styles. + // This will be triggered by adding an HTML element to the native html element + // to avoid native interfaces needing to call into Java which is a hassle. + private boolean matchCN1Style; + + public HTML5Peer(HTMLElement el){ + super(el); + //this.el = el; + String cssClass = el.getAttribute("class"); + if (cssClass == null){ + cssClass = ""; + } + cssClass += "cn1-native-peer"; + if (HTML5Implementation.getInstance().paintNativePeersBehind()) { + el.getStyle().setProperty("z-index", "-1000"); + } + el.setAttribute("class", cssClass); + if (el.hasAttribute("data-cn1-match-style")) { + matchCN1Style = true; + applyStyle(getStyle()); + } + + + } + + protected HTMLElement el() { + return (HTMLElement)getNativePeer(); + } + + private void applyStyle(Style source) { + HTMLElement el = el(); + Font f = source.getFont(); + NativeFont nf = (NativeFont)f.getNativeFont(); + el.getStyle().setProperty("font", nf.getScaledCSS()); + + el.getStyle().setProperty("padding", + unscaleCoord(source.getPaddingTop())+"px " + +unscaleCoord(source.getPaddingRight(false))+"px " + +unscaleCoord(source.getPaddingBottom())+"px " + +unscaleCoord(source.getPaddingLeft(false))+"px"); + int fgColor = source.getFgColor(); + int r = ColorUtil.red(fgColor); + int g = ColorUtil.green(fgColor); + int b = ColorUtil.blue(fgColor); + + el.getStyle().setProperty("color", "rgb("+r+","+g+","+b+")"); + } + + @Override + public void styleChanged(String propertyName, Style source) { + super.styleChanged(propertyName, source); + if (!matchCN1Style || getParent() == null) { + return; + } + + if (source != getParent().getStyle()) { + return; + } + HTMLElement el = el(); + if (Style.FONT.equals(propertyName)) { + Font f = source.getFont(); + NativeFont nf = (NativeFont)f.getNativeFont(); + el.getStyle().setProperty("font", nf.getScaledCSS()); + getStyle().setFont(f); + return; + } + + if (Style.PADDING.equals(propertyName)) { + el.getStyle().setProperty("padding", + unscaleCoord(source.getPaddingTop())+"px " + +unscaleCoord(source.getPaddingRight(false))+"px " + +unscaleCoord(source.getPaddingBottom())+"px " + +unscaleCoord(source.getPaddingLeft(false))+"px"); + getStyle().setPaddingLeft(source.getPaddingLeft(true)); + getStyle().setPaddingRight(source.getPaddingRight(true)); + getStyle().setPaddingTop(source.getPaddingTop()); + getStyle().setPaddingBottom(source.getPaddingBottom()); + return; + } + if (Style.FG_COLOR.equals(propertyName)) { + int fgColor = source.getFgColor(); + int r = ColorUtil.red(fgColor); + int g = ColorUtil.green(fgColor); + int b = ColorUtil.blue(fgColor); + + el.getStyle().setProperty("color", "rgb("+r+","+g+","+b+")"); + getStyle().setFgColor(fgColor); + return; + } + + if (Style.BG_COLOR.equals(propertyName)) { + int fgColor = source.getBgColor(); + int r = ColorUtil.red(fgColor); + int g = ColorUtil.green(fgColor); + int b = ColorUtil.blue(fgColor); + + el.getStyle().setProperty("background-color", "rgb("+r+","+g+","+b+")"); + getStyle().setBgColor(fgColor); + return; + } + + if (Style.BORDER.equals(propertyName)) { + Border border = source.getBorder(); + if (border instanceof RoundRectBorder) { + RoundRectBorder rrb = (RoundRectBorder)border; + el.getStyle().setProperty("border-radius", unscaleCoord(CN.convertToPixels(rrb.getCornerRadius()))+"px"); + el.getStyle().setProperty("border-width", unscaleCoord(CN.convertToPixels(rrb.getStrokeThickness()))+"px"); + int fgColor = rrb.getStrokeColor(); + int r = ColorUtil.red(fgColor); + int g = ColorUtil.green(fgColor); + int b = ColorUtil.blue(fgColor); + int a = rrb.getStrokeOpacity(); + + el.getStyle().setProperty("border-color", "rgba("+r+","+g+","+b+","+a+")"); + } else { + el.getStyle().setProperty("border-radius", "0"); + el.getStyle().setProperty("border-width", unscaleCoord(border.getThickness())+"px"); + int fgColor = (int)border.getProperty("ColorA"); + int r = ColorUtil.red(fgColor); + int g = ColorUtil.green(fgColor); + int b = ColorUtil.blue(fgColor); + el.getStyle().setProperty("border-color", "rgb("+r+","+g+","+b+")"); + el.getStyle().setProperty("border-color", propertyName); + } + } + + + + } + + + + @Override + public Style getPressedStyle() { + + return super.getPressedStyle(); //To change body of generated methods, choose Tools | Templates. + } + + + + @JSBody(params={"el"}, script="return jQuery(el).outerWidth()") + private native static int outerWidth(HTMLElement el); + + + @JSBody(params={"el"}, script="return jQuery(el).outerHeight()") + private native static int outerHeight(HTMLElement el); + + private static boolean isTextInputType(String type) { + return ("text".equals(type) || "email".equals(type) || "password".equals(type) || "search".equals(type) || "tel".equals(type) || "url".equals(type)); + } + + @JSBody(params={"el"}, script="return el.tagName") + private native static String tagName(HTMLElement el); + + @Override + protected Dimension calcPreferredSize() { + HTMLElement el = el(); + HTML5Implementation._debugObj(el); + if (el == null || JS.isUndefined(el)) { + return super.calcPreferredSize(); + } + + if ("iframe".equalsIgnoreCase(tagName(el))) { + return new Dimension(CN.getDisplayWidth(), CN.getDisplayHeight()); + } + + if ("video".equalsIgnoreCase(tagName(el))) { + + return new Dimension(640, 480); + } + + if ("audio".equalsIgnoreCase(tagName(el))) { + return new Dimension(640, getStyle().getFont().getHeight() * 2); + } + + if ("input".equalsIgnoreCase(tagName(el))) { + com.codename1.ui.Font f = getStyle().getFont(); + int h = (int)Math.round(f.getHeight() * 1.8 + getStyle().getVerticalPadding()); + int charW = f.charWidth('M'); + int w = charW * 30; + if (isTextInputType(el.getAttribute("type")) && el.hasAttribute("size")) { + w = charW * Integer.parseInt(el.getAttribute("size")); + } + w += getStyle().getHorizontalPadding(); + return new Dimension(w, h); + } + + if ("select".equalsIgnoreCase(tagName(el))) { + com.codename1.ui.Font f = getStyle().getFont(); + int h = (int)Math.round(f.getHeight() * 1.8 + getStyle().getVerticalPadding()); + int charW = f.charWidth('M'); + int w = charW * 30; + NodeList opts = el.querySelectorAll("option"); + int len = opts.getLength(); + int maxOptionLength = 0; + for (int i=0; i jactions = JSArray.create(len); + for (int i=0; i convertPushActionCategoriesToJSArray(PushActionCategory[] categories) { + int len = categories.length; + JSArray out = JSArray.create(len); + for (int i=0; i pushActionCategories = null; + if (pushCallback instanceof PushActionsProvider) { + pushActionCategories = convertPushActionCategoriesToJSArray(((PushActionsProvider)pushCallback).getPushActionCategories()); + } + registerPushNative(onSuccess, onFail, onPush, pushActionCategories); + } +} diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java new file mode 100644 index 0000000000..6b3f990e22 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/JSOImplementations.java @@ -0,0 +1,805 @@ +/* + * Copyright (c) 2026 Codename One and contributors. + * Licensed under the PolyForm Noncommercial License 1.0.0. + * You may use this file only in compliance with that license. + * The license notice for this subtree is available in Ports/JavaScriptPort/LICENSE.md. + */ + +package com.codename1.impl.html5; + +import com.codename1.teavm.jso.io.Blob; +import org.teavm.jso.JSBody; + +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.browser.Window; +import org.teavm.jso.core.JSString; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.events.MouseEvent; +import org.teavm.jso.dom.html.HTMLCanvasElement; +import org.teavm.jso.dom.html.HTMLDocument; +import org.teavm.jso.dom.html.HTMLElement; +import org.teavm.jso.dom.html.HTMLImageElement; +import org.teavm.jso.dom.html.HTMLInputElement; +import org.teavm.jso.typedarrays.ArrayBuffer; + +/** + * + * @author shannah + */ +public class JSOImplementations { + + + @JSFunctor + public interface AnimationFrameCallback extends JSObject { + void onAnimationFrame(int time); + } + + public interface Console extends JSObject { + void log(String str); + } + + + public interface JSFontMetrics extends JSObject { + @JSProperty + double getAscent(); + + @JSProperty + double getDescent(); + + @JSProperty + double getHeight(); + + @JSProperty + double getLeading(); + + @JSProperty + JSObject getBounds(); + } + + public interface WindowExt extends JSObject { + + + @JSProperty + abstract Console getConsole(); + + abstract int requestAnimationFrame(AnimationFrameCallback callback); + + abstract String eval(String str); + + @JSProperty + abstract public int getTEMPORARY(); + + @JSProperty + abstract public int getPERSISTENT(); + + abstract void requestFileSystem(int type, int size, FileSystemCallback success, ErrorCallback error); + + @JSProperty + abstract public WebkitStorageInfo getWebkitStorageInfo(); + + abstract public void requestFileSystem(int persistent, int grantedBytes, FileSystemCallback fileSystemCallback); + + @JSBody(params={}, script="return new Object()") + JSObject createEmptyObject(); + + @JSProperty + abstract public CN1Native getCn1(); + + @JSBody(params={}, script="return new FileReader()") + public FileReader createFileReader(); + + @JSBody(params={"obj"}, script="return new FileWriter(obj)") + public FileWriter createFileWriter(JSObject obj); + + @JSBody(params={"o"}, script="return new FileSaver(o)") + public FileSaver createFileSaver(JSObject obj); + + @JSProperty + abstract public Navigator getNavigator(); + + abstract public Blob Base64ToBlob(String dataURL); + + abstract public void BlobToBase64(Blob blob, DataURLCallback callback); + + //abstract public String encodeURIComponent(String uri); + + /** + * Gets the URL to use for the CORS proxy. This will be used + * In the stub by default. Implementing in Javascript allows + * us to more easily override it after the app is compiled. + * This can be overridden by adding the following in a + + +

Kitchen Sink Demo

+
This is a native HTML web component embedded in the Codename One + application. You can navigate to the Codename One website by clicking the link above. you can add + stuff into the page from Java using the UI above.
+
+ Execute Java code from the web browser by pressing here. +
+
+ +
+ + diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/android_holo_light.res b/Ports/JavaScriptPort/src/main/webapp/assets/android_holo_light.res new file mode 100644 index 0000000000..056cd2504a Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/android_holo_light.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/chrome.res b/Ports/JavaScriptPort/src/main/webapp/assets/chrome.res new file mode 100644 index 0000000000..055e42db33 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/chrome.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/iOS7Theme.res b/Ports/JavaScriptPort/src/main/webapp/assets/iOS7Theme.res new file mode 100644 index 0000000000..83e067b696 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/iOS7Theme.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/iPhoneTheme.res b/Ports/JavaScriptPort/src/main/webapp/assets/iPhoneTheme.res new file mode 100644 index 0000000000..de9eb1d035 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/iPhoneTheme.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/leather.res b/Ports/JavaScriptPort/src/main/webapp/assets/leather.res new file mode 100644 index 0000000000..816eafa531 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/leather.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/test.json b/Ports/JavaScriptPort/src/main/webapp/assets/test.json new file mode 100644 index 0000000000..d5ca56d195 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/assets/test.json @@ -0,0 +1,22 @@ +{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } +} \ No newline at end of file diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/theme.res b/Ports/JavaScriptPort/src/main/webapp/assets/theme.res new file mode 100644 index 0000000000..bf748cd951 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/theme.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/tzone_theme.res b/Ports/JavaScriptPort/src/main/webapp/assets/tzone_theme.res new file mode 100644 index 0000000000..63c28555ac Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/tzone_theme.res differ diff --git a/Ports/JavaScriptPort/src/main/webapp/assets/video.mp4 b/Ports/JavaScriptPort/src/main/webapp/assets/video.mp4 new file mode 100644 index 0000000000..40c3a8e699 Binary files /dev/null and b/Ports/JavaScriptPort/src/main/webapp/assets/video.mp4 differ diff --git a/Ports/JavaScriptPort/src/main/webapp/css/bootstrap-theme.min.css b/Ports/JavaScriptPort/src/main/webapp/css/bootstrap-theme.min.css new file mode 100644 index 0000000000..cefa3d1ae3 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/css/bootstrap-theme.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.4 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} \ No newline at end of file diff --git a/Ports/JavaScriptPort/src/main/webapp/css/bootstrap.min.css b/Ports/JavaScriptPort/src/main/webapp/css/bootstrap.min.css new file mode 100644 index 0000000000..cd1c616ad8 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/css/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.4 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px \9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.form-group-sm .form-control{height:30px;line-height:30px}select[multiple].form-group-sm .form-control,textarea.form-group-sm .form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.form-group-lg .form-control{height:46px;line-height:46px}select[multiple].form-group-lg .form-control,textarea.form-group-lg .form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:10px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.active,.btn-default.focus,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px)and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px)and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px)and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px)and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px)and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px)and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px)and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px)and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px)and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px)and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px)and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/Ports/JavaScriptPort/src/main/webapp/index.html b/Ports/JavaScriptPort/src/main/webapp/index.html new file mode 100755 index 0000000000..8997e3083b --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/index.html @@ -0,0 +1,203 @@ + + + + + {APP_TITLE} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

...Loading...

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ports/JavaScriptPort/src/main/webapp/js/bootstrap.min.js b/Ports/JavaScriptPort/src/main/webapp/js/bootstrap.min.js new file mode 100644 index 0000000000..c8f82e592a --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.3.4 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file diff --git a/Ports/JavaScriptPort/src/main/webapp/js/fontmetrics.js b/Ports/JavaScriptPort/src/main/webapp/js/fontmetrics.js new file mode 100755 index 0000000000..91f318edf9 --- /dev/null +++ b/Ports/JavaScriptPort/src/main/webapp/js/fontmetrics.js @@ -0,0 +1,3122 @@ + +// This flag affects drawString. If it is true, then drawString will use the alphabetic text baseline +// Otherwise it will use the top text baseline. We used to use the top text baseline, +// but there are inconsistencies between Firefox and Chrome. Chrome seems to add an offset. +// If we use the alphabetic baseline and simply add the ascent and leading, then we +// get more consistent results. +// I am leaving this as a javacript flag to make it easier to toggle and experiment +// at runtime. +window.cn1_use_baseline_text_rendering =true; +/*window.cn1_debug_flags = window.cn1_debug_flags || {}; +window.cn1_debug_flags.debugLog = true; +setTimeout(function() { + window.cn1_debug_flags.debugLog = true; +}, 1000);*/ +(function () { + + if ( typeof window.CustomEvent === "function" ) return false; + + function CustomEvent ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(); + +window.cn1_escape_single_quotes = function(str) { + return String(str).replace(/\\/g, "\\\\").replace(/'/g, "\\'"); +}; +window.cn1NativeBacksideHooks = []; +window.cn1RunOnMainThread = function(callback) { + window.cn1NativeBacksideHooks.push(callback); +}; + +(function() { + // This section defines a cn1RunPrivileged() method which is a hack that allows us to run a function with MEI + // approval in iOS and Safari. + // This may also be useful on Android but haven't tested yet. + var isSafari = _isSafari(); + function _isSafari() { + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf('safari') != -1) { + if (ua.indexOf('chrome') > -1) { + //return false; + } else { + return true; + } + } + return false; + } + + function isIOS() { + + return (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + } + // Default runPrivileged just runs the function directly. + window.cn1RunPrivileged = function(callback) { + callback(); + }; + + // If this isn't iOS and Safari, we don't need any of the rest. + if (!isSafari && !isIOS()) return; + + // + var unlockedSilentAudio; + var unlockedSilentAudioPlayQueue = []; + + // Attempt to unlock an audio clip + function unlockAudioClip(audio) { + try { + audio.setAttribute('data-cn1-unlocked', 'true'); + var testPlay = audio.play(); + if (testPlay && typeof Promise !== 'undefined' && (testPlay instanceof Promise || typeof testPlay.then === 'function')) { + testPlay.then(function() { + }, function(err) { + if (err.name=='NotAllowedError' || err.name == 'AbortError') { + unlockedSilentAudio = null; + } + }); + } + } catch (err) { + + } + + } + function unlockAudio() { + try { + // Set flag to indicate that we're unlocking clips + // Used in HTMLMediaElement.play() method override to know not to do anything + window.cn1UnlockingClips = true; + + // Remove all event handlers that were set up to unlock the audio. + document.removeEventListener('touchstart', unlockAudio, true); + document.removeEventListener('touchend', unlockAudio, true); + document.removeEventListener('click', unlockAudio, true); + window.removeEventListener('installbacksidehooks', unlockAudio, true); + + if (!unlockedSilentAudio) { + initSilentAudio(); + unlockAudioClip(unlockedSilentAudio); + } + } finally { + window.cn1UnlockingClips = false; + } + + }; + + // Add listeners to unlock the silent audio clip. + // We need the clip to be unlocked in an event to get around MEI restrictions + document.addEventListener('touchstart', unlockAudio, true); + document.addEventListener('touchend', unlockAudio, true); + document.addEventListener('click', unlockAudio, true); + + // the installbacksidehooks event is a synthetic event that is intercepted + // by CN1 to install backside hooks during valid user event. + window.addEventListener('installbacksidehooks', unlockAudio, true); + + function initSilentAudio() { + if (!unlockedSilentAudio) { + unlockedSilentAudio = new Audio(); + unlockedSilentAudio.src = cn1CreateSilentAudio(0.1); + unlockedSilentAudio.addEventListener('playing', function() { + ///console.log("Unlocked silent audio is playing"); + }, true); + + unlockedSilentAudio.addEventListener('ended', function() { + //console.log("Silend audio ended"); + while (unlockedSilentAudioPlayQueue.length > 0) { + var f = unlockedSilentAudioPlayQueue.pop(); + setTimeout(f,0); + } + }, true); + } + } + + + // Assign runPrivileged to global cn1RunPrivileged method + window.cn1RunPrivileged = runPrivileged; + + // Function ro run callback in the 'ended' listener of the silent audio + // clip. Since the silent audio clip is unlocked, the ended event + // will run with loosened MEI restrictions. + // THis is a bit of a hack, but it seems to work. + function runPrivileged(callback) { + if (!unlockedSilentAudio) { + callback(); + return; + } + var complete = false; + var timeout = setTimeout(function() { + if (complete) return; + complete = true; + console.log('WARNING: runPrivileged() reached timeout', callback); + callback(); + }, 1000); + unlockedSilentAudioPlayQueue.push(function() { + if (complete) return; + complete = true; + clearTimeout(timeout); + callback(); + }); + unlockedSilentAudio.play(); + + } + + // We want to override the regular HTMLMediaElement.play() method + // so that it runs privileged. But we only want to do this for CN1 media clips. + var origPlay = HTMLMediaElement.prototype.play; + var alreadyMadeFirstPlay = false; + + var newPlay = function() { + if (window.cn1UnlockingClips || !unlockedSilentAudio || (unlockedSilentAudio && this === unlockedSilentAudio)) { + //console.log("HTMLMediaElement.play| using origPlay to play media because unlocking in progress, or there is no unlockedSilentAudio, or this is the unlockedSilentAudio", this); + return origPlay.apply(this); + } + if (this.getAttribute('data-cn1-unlocked')) { + console.log("HTMLMedia.play|already-unlocked", this); + return origPlay.apply(this).then(function() { + console.log("HTMLMedia.play|already-unlocked >> successful play"); + }); + } + + + var mediaId = this.getAttribute('cn1-audio-id'); + var video = this.tagName.toLowerCase() === 'video'; + console.log("HTMLMediaElement.play", this); + if (video && !alreadyMadeFirstPlay) { + console.log("HTMLMediaElement.play| using origPlay to play video it could be the 'test' play.", this); + alreadyMadeFirstPlay = true; + return origPlay.apply(this); + } + + if (!video && !mediaId) { + console.log("HTMLMediaElement.play| using origPlay to play audio because element has no mediaId attribute ", this); + return origPlay.apply(this); + } + + + + var self = this; + alreadyMadeFirstPlay = true; + + //console.log("Requesting to play clip", this); + return new Promise(function(resolve, reject) { + runPrivileged(function() { + origPlay.apply(self).then(resolve, reject); + }); + + }); + + + } + + HTMLMediaElement.prototype.play = newPlay; + +})(); + +/* + * Copyright 2016 Small Batch, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +/* Web Font Loader v1.6.26 - (c) Adobe Systems, Google. License: Apache 2.0 */(function(){function aa(a,b,c){return a.call.apply(a.bind,arguments)}function ba(a,b,c){if(!a)throw Error();if(2=b.f?e():a.fonts.load(fa(b.a),b.h).then(function(a){1<=a.length?d():setTimeout(k,25)},function(){e()})}k()}),e=new Promise(function(a,d){setTimeout(d,b.f)});Promise.race([e,d]).then(function(){b.g(b.a)},function(){b.j(b.a)})};function R(a,b,c,d,e,f,g){this.v=a;this.B=b;this.c=c;this.a=d;this.s=g||"BESbswy";this.f={};this.w=e||3E3;this.u=f||null;this.o=this.j=this.h=this.g=null;this.g=new N(this.c,this.s);this.h=new N(this.c,this.s);this.j=new N(this.c,this.s);this.o=new N(this.c,this.s);a=new H(this.a.c+",serif",K(this.a));a=P(a);this.g.a.style.cssText=a;a=new H(this.a.c+",sans-serif",K(this.a));a=P(a);this.h.a.style.cssText=a;a=new H("serif",K(this.a));a=P(a);this.j.a.style.cssText=a;a=new H("sans-serif",K(this.a));a= +P(a);this.o.a.style.cssText=a;O(this.g);O(this.h);O(this.j);O(this.o)}var S={D:"serif",C:"sans-serif"},T=null;function U(){if(null===T){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);T=!!a&&(536>parseInt(a[1],10)||536===parseInt(a[1],10)&&11>=parseInt(a[2],10))}return T}R.prototype.start=function(){this.f.serif=this.j.a.offsetWidth;this.f["sans-serif"]=this.o.a.offsetWidth;this.A=q();la(this)}; +function ma(a,b,c){for(var d in S)if(S.hasOwnProperty(d)&&b===a.f[S[d]]&&c===a.f[S[d]])return!0;return!1}function la(a){var b=a.g.a.offsetWidth,c=a.h.a.offsetWidth,d;(d=b===a.f.serif&&c===a.f["sans-serif"])||(d=U()&&ma(a,b,c));d?q()-a.A>=a.w?U()&&ma(a,b,c)&&(null===a.u||a.u.hasOwnProperty(a.a.c))?V(a,a.v):V(a,a.B):na(a):V(a,a.v)}function na(a){setTimeout(p(function(){la(this)},a),50)}function V(a,b){setTimeout(p(function(){v(this.g.a);v(this.h.a);v(this.j.a);v(this.o.a);b(this.a)},a),0)};function W(a,b,c){this.c=a;this.a=b;this.f=0;this.o=this.j=!1;this.s=c}var X=null;W.prototype.g=function(a){var b=this.a;b.g&&w(b.f,[b.a.c("wf",a.c,K(a).toString(),"active")],[b.a.c("wf",a.c,K(a).toString(),"loading"),b.a.c("wf",a.c,K(a).toString(),"inactive")]);L(b,"fontactive",a);this.o=!0;oa(this)}; +W.prototype.h=function(a){var b=this.a;if(b.g){var c=y(b.f,b.a.c("wf",a.c,K(a).toString(),"active")),d=[],e=[b.a.c("wf",a.c,K(a).toString(),"loading")];c||d.push(b.a.c("wf",a.c,K(a).toString(),"inactive"));w(b.f,d,e)}L(b,"fontinactive",a);oa(this)};function oa(a){0==--a.f&&a.j&&(a.o?(a=a.a,a.g&&w(a.f,[a.a.c("wf","active")],[a.a.c("wf","loading"),a.a.c("wf","inactive")]),L(a,"active")):M(a.a))};function pa(a){this.j=a;this.a=new ja;this.h=0;this.f=this.g=!0}pa.prototype.load=function(a){this.c=new ca(this.j,a.context||this.j);this.g=!1!==a.events;this.f=!1!==a.classes;qa(this,new ha(this.c,a),a)}; +function ra(a,b,c,d,e){var f=0==--a.h;(a.f||a.g)&&setTimeout(function(){var a=e||null,k=d||null||{};if(0===c.length&&f)M(b.a);else{b.f+=c.length;f&&(b.j=f);var h,m=[];for(h=0;h= 7) { + * // IE7 or better + * } + * + * The browser functions will return NaN if the browser does not match, so + * you can also do version compares the other way: + * + * if (UserAgent_DEPRECATED.ie() < 7) { + * // IE6 or worse + * } + * + * Note that the version is a float and may include a minor version number, + * so you should always use range operators to perform comparisons, not + * strict equality. + * + * **Note:** You should **strongly** prefer capability detection to browser + * version detection where it's reasonable: + * + * http://www.quirksmode.org/js/support.html + * + * Further, we have a large number of mature wrapper functions and classes + * which abstract away many browser irregularities. Check the documentation, + * grep for things, or ask on javascript@lists.facebook.com before writing yet + * another copy of "event || window.event". + * + */ + + var _populated = false; + + // Browsers + var _ie, _firefox, _opera, _webkit, _chrome; + + // Actual IE browser for compatibility mode + var _ie_real_version; + + // Platforms + var _osx, _windows, _linux, _android; + + // Architectures + var _win64; + + // Devices + var _iphone, _ipad, _native; + + var _mobile; + + function _populate() { + if (_populated) { + return; + } + + _populated = true; + + // To work around buggy JS libraries that can't handle multi-digit + // version numbers, Opera 10's user agent string claims it's Opera + // 9, then later includes a Version/X.Y field: + // + // Opera/9.80 (foo) Presto/2.2.15 Version/10.10 + var uas = navigator.userAgent; + var agent = /(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(uas); + var os = /(Mac OS X)|(Windows)|(Linux)/.exec(uas); + + _iphone = /\b(iPhone|iP[ao]d)/.exec(uas); + _ipad = /\b(iP[ao]d)/.exec(uas) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + _android = /Android/i.exec(uas); + _native = /FBAN\/\w+;/i.exec(uas); + _mobile = /Mobile/i.exec(uas); + + // Note that the IE team blog would have you believe you should be checking + // for 'Win64; x64'. But MSDN then reveals that you can actually be coming + // from either x64 or ia64; so ultimately, you should just check for Win64 + // as in indicator of whether you're in 64-bit IE. 32-bit IE on 64-bit + // Windows will send 'WOW64' instead. + _win64 = !!(/Win64/.exec(uas)); + + if (agent) { + _ie = agent[1] ? parseFloat(agent[1]) : ( + agent[5] ? parseFloat(agent[5]) : NaN); + // IE compatibility mode + if (_ie && document && document.documentMode) { + _ie = document.documentMode; + } + // grab the "true" ie version from the trident token if available + var trident = /(?:Trident\/(\d+.\d+))/.exec(uas); + _ie_real_version = trident ? parseFloat(trident[1]) + 4 : _ie; + + _firefox = agent[2] ? parseFloat(agent[2]) : NaN; + _opera = agent[3] ? parseFloat(agent[3]) : NaN; + _webkit = agent[4] ? parseFloat(agent[4]) : NaN; + if (_webkit) { + // We do not add the regexp to the above test, because it will always + // match 'safari' only since 'AppleWebKit' appears before 'Chrome' in + // the userAgent string. + agent = /(?:Chrome\/(\d+\.\d+))/.exec(uas); + _chrome = agent && agent[1] ? parseFloat(agent[1]) : NaN; + } else { + _chrome = NaN; + } + } else { + _ie = _firefox = _opera = _chrome = _webkit = NaN; + } + + if (os) { + if (os[1]) { + // Detect OS X version. If no version number matches, set _osx to true. + // Version examples: 10, 10_6_1, 10.7 + // Parses version number as a float, taking only first two sets of + // digits. If only one set of digits is found, returns just the major + // version number. + var ver = /(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(uas); + + _osx = ver ? parseFloat(ver[1].replace('_', '.')) : true; + } else { + _osx = false; + } + _windows = !!os[2]; + _linux = !!os[3]; + } else { + _osx = _windows = _linux = false; + } + } + + window.UserAgent_DEPRECATED = { + + /** + * Check if the UA is Internet Explorer. + * + * + * @return float|NaN Version number (if match) or NaN. + */ + ie: function() { + return _populate() || _ie; + }, + + /** + * Check if we're in Internet Explorer compatibility mode. + * + * @return bool true if in compatibility mode, false if + * not compatibility mode or not ie + */ + ieCompatibilityMode: function() { + return _populate() || (_ie_real_version > _ie); + }, + + + /** + * Whether the browser is 64-bit IE. Really, this is kind of weak sauce; we + * only need this because Skype can't handle 64-bit IE yet. We need to remove + * this when we don't need it -- tracked by #601957. + */ + ie64: function() { + return UserAgent_DEPRECATED.ie() && _win64; + }, + + /** + * Check if the UA is Firefox. + * + * + * @return float|NaN Version number (if match) or NaN. + */ + firefox: function() { + return _populate() || _firefox; + }, + + + /** + * Check if the UA is Opera. + * + * + * @return float|NaN Version number (if match) or NaN. + */ + opera: function() { + return _populate() || _opera; + }, + + + /** + * Check if the UA is WebKit. + * + * + * @return float|NaN Version number (if match) or NaN. + */ + webkit: function() { + return _populate() || _webkit; + }, + + /** + * For Push + * WILL BE REMOVED VERY SOON. Use UserAgent_DEPRECATED.webkit + */ + safari: function() { + return UserAgent_DEPRECATED.webkit(); + }, + + /** + * Check if the UA is a Chrome browser. + * + * + * @return float|NaN Version number (if match) or NaN. + */ + chrome : function() { + return _populate() || _chrome; + }, + + + /** + * Check if the user is running Windows. + * + * @return bool `true' if the user's OS is Windows. + */ + windows: function() { + return _populate() || _windows; + }, + + + /** + * Check if the user is running Mac OS X. + * + * @return float|bool Returns a float if a version number is detected, + * otherwise true/false. + */ + osx: function() { + return _populate() || _osx; + }, + + /** + * Check if the user is running Linux. + * + * @return bool `true' if the user's OS is some flavor of Linux. + */ + linux: function() { + return _populate() || _linux; + }, + + /** + * Check if the user is running on an iPhone or iPod platform. + * + * @return bool `true' if the user is running some flavor of the + * iPhone OS. + */ + iphone: function() { + return _populate() || _iphone; + }, + + mobile: function() { + return _populate() || (_iphone || _ipad || _android || _mobile); + }, + + nativeApp: function() { + // webviews inside of the native apps + return _populate() || _native; + }, + + android: function() { + return _populate() || _android; + }, + + ipad: function() { + return _populate() || _ipad; + } + }; + +})(); + +(function(){ + //Mouse wheel events are inconsistent across browsers + // This function will normalize it + //https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js + /** + * Check if an event is supported. + * Ref: http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + */ + function isEventSupported(event) { + var testEl = document.createElement('div'); + var isSupported; + + event = 'on' + event; + isSupported = (event in testEl); + + if (!isSupported) { + testEl.setAttribute(event, 'return;'); + isSupported = typeof testEl[event] === 'function'; + } + testEl = null; + + return isSupported; + } + // Reasonable defaults + var PIXEL_STEP = 10; + var LINE_HEIGHT = 40; + var PAGE_HEIGHT = 800; + function normalizeWheel(/*object*/ event) /*object*/ { + var sX = 0, sY = 0, // spinX, spinY + pX = 0, pY = 0; // pixelX, pixelY + + // Legacy + if ('detail' in event) { sY = event.detail; } + if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; } + if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; } + if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; } + + // side scrolling on FF with DOMMouseScroll + if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) { + sX = sY; + sY = 0; + } + + pX = sX * PIXEL_STEP; + pY = sY * PIXEL_STEP; + + if ('deltaY' in event) { pY = event.deltaY; } + if ('deltaX' in event) { pX = event.deltaX; } + + if ((pX || pY) && event.deltaMode) { + if (event.deltaMode == 1) { // delta in LINE units + pX *= LINE_HEIGHT; + pY *= LINE_HEIGHT; + } else { // delta in PAGE units + pX *= PAGE_HEIGHT; + pY *= PAGE_HEIGHT; + } + } + + // Fall-back if spin cannot be determined + if (pX && !sX) { sX = (pX < 1) ? -1 : 1; } + if (pY && !sY) { sY = (pY < 1) ? -1 : 1; } + + return { spinX : sX, + spinY : sY, + pixelX : pX, + pixelY : pY }; + } + + normalizeWheel.getEventType = function() /*string*/ { + return (UserAgent_DEPRECATED.firefox()) + ? 'DOMMouseScroll' + : (isEventSupported('wheel')) + ? 'wheel' + : 'mousewheel'; + }; + + window.cn1NormalizeWheel = normalizeWheel; +})(); + +window.copyWheelEvent = function(event, iframe, x, y) { + var type = event.type == 'MozMousePixelScroll' ? 'DOMMouseScroll' : event.type; + var evt = new CustomEvent(type, {bubbles: true, cancelable: true}); + evt.clientX = event.clientX + x; + evt.clientY = event.clientY + y; + if ('axis' in event) evt.axis = event.axis; + evt.cn1Detail = event.detail; + if ('deltaY' in event) evt.deltaY = event.deltaY; + if ('deltaX' in event) evt.deltaX = event.deltaX; + if ('wheelDelta' in event) evt.wheelDelta = event.wheelDelta; + if ('wheelDeltaX' in event) evt.wheelDeltaX = event.wheelDeltaX; + if ('wheelDeltaY' in event) { + evt.wheelDeltaY = event.wheelDeltaY; + } else if ('detail' in event) { + // Firefox.. we can't set detail, so we need to fake the wheelDeltaY + // so that the normalizeWheel method will work + evt.wheelDeltaY = - event.detail * 120; + } + //console.log('wheel event', event, evt, event.wheelDeltaY); + return evt; +}; + +window.copyTouchEvent = function(event, iframe, x, y) { + //console.log("Copying touch event" + event); + var evt = new CustomEvent(event.type, {bubbles: true, cancelable: true}); + if ('clientX' in event) evt.clientX = event.clientX + x; + if ('clientY' in event) evt.clientY = event.clientY + y; + if ('changedTouches' in event) { + var touches = []; + for (var i=0; i + * [ x'] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ] + * [ y'] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ] + * [ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ] + * + * + * This class is optimized for speed and minimizes calculations based on its + * knowledge of the underlying matrix (as opposed to say simply performing + * matrix multiplication). + * + * @param {number} opt_m00 The m00 coordinate of the transform. + * @param {number} opt_m10 The m10 coordinate of the transform. + * @param {number} opt_m01 The m01 coordinate of the transform. + * @param {number} opt_m11 The m11 coordinate of the transform. + * @param {number} opt_m02 The m02 coordinate of the transform. + * @param {number} opt_m12 The m12 coordinate of the transform. + * @constructor + */ +goog.graphics.AffineTransform = function(opt_m00, opt_m10, opt_m01, + opt_m11, opt_m02, opt_m12) { + if (arguments.length == 6) { + this.setTransform(/** @type {number} */ (opt_m00), + /** @type {number} */ (opt_m10), + /** @type {number} */ (opt_m01), + /** @type {number} */ (opt_m11), + /** @type {number} */ (opt_m02), + /** @type {number} */ (opt_m12)); + } else if (arguments.length != 0) { + throw Error('Insufficient matrix parameters'); + } else { + this.m00_ = this.m11_ = 1; + this.m10_ = this.m01_ = this.m02_ = this.m12_ = 0; + } +}; + + +/** + * @return {boolean} Whether this transform is the identity transform. + */ +goog.graphics.AffineTransform.prototype.isIdentity = function() { + return this.m00_ == 1 && this.m10_ == 0 && this.m01_ == 0 && + this.m11_ == 1 && this.m02_ == 0 && this.m12_ == 0; +}; + + +/** + * @return {!goog.graphics.AffineTransform} A copy of this transform. + */ +goog.graphics.AffineTransform.prototype.cloneTransform = function() { + return new goog.graphics.AffineTransform(this.m00_, this.m10_, this.m01_, + this.m11_, this.m02_, this.m12_); +}; + + +/** + * Sets this transform to the matrix specified by the 6 values. + * + * @param {number} m00 The m00 coordinate of the transform. + * @param {number} m10 The m10 coordinate of the transform. + * @param {number} m01 The m01 coordinate of the transform. + * @param {number} m11 The m11 coordinate of the transform. + * @param {number} m02 The m02 coordinate of the transform. + * @param {number} m12 The m12 coordinate of the transform. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.setTransform = function(m00, m10, m01, + m11, m02, m12) { + //if (!goog.isNumber(m00) || !goog.isNumber(m10) || !goog.isNumber(m01) || + // !goog.isNumber(m11) || !goog.isNumber(m02) || !goog.isNumber(m12)) { + // throw Error('Invalid transform parameters'); + //} + this.m00_ = m00; + this.m10_ = m10; + this.m01_ = m01; + this.m11_ = m11; + this.m02_ = m02; + this.m12_ = m12; + return this; +}; + + +/** + * Sets this transform to be identical to the given transform. + * + * @param {!goog.graphics.AffineTransform} tx The transform to copy. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.copyFrom = function(tx) { + this.m00_ = tx.m00_; + this.m10_ = tx.m10_; + this.m01_ = tx.m01_; + this.m11_ = tx.m11_; + this.m02_ = tx.m02_; + this.m12_ = tx.m12_; + return this; +}; + + +/** + * Concatentates this transform with a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.scale = function(sx, sy) { + this.m00_ *= sx; + this.m10_ *= sx; + this.m01_ *= sy; + this.m11_ *= sy; + return this; +}; + + +/** + * Concatentates this transform with a translate transformation. + * + * @param {number} dx The distance to translate in the x direction. + * @param {number} dy The distance to translate in the y direction. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.translate = function(dx, dy) { + this.m02_ += dx * this.m00_ + dy * this.m01_; + this.m12_ += dx * this.m10_ + dy * this.m11_; + return this; +}; + + +/** + * Concatentates this transform with a rotation transformation around an anchor + * point. + * + * @param {number} theta The angle of rotation measured in radians. + * @param {number} x The x coordinate of the anchor point. + * @param {number} y The y coordinate of the anchor point. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.rotate = function(theta, x, y) { + return this.concatenate( + goog.graphics.AffineTransform.getRotateInstance(theta, x, y)); +}; + + +/** + * Concatentates this transform with a shear transformation. + * + * @param {number} shx The x shear factor. + * @param {number} shy The y shear factor. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.shear = function(shx, shy) { + var m00 = this.m00_; + var m10 = this.m10_; + this.m00_ += shy * this.m01_; + this.m10_ += shy * this.m11_; + this.m01_ += shx * m00; + this.m11_ += shx * m10; + return this; +}; + + +/** + * @return {string} A string representation of this transform. The format of + * of the string is compatible with SVG matrix notation, i.e. + * "matrix(a,b,c,d,e,f)". + */ +goog.graphics.AffineTransform.prototype.stringValue = function() { + return 'matrix(' + [this.m00_, this.m10_, this.m01_, this.m11_, + this.m02_, this.m12_].join(',') + ')'; +}; + + + +/** + * @return {number} The scaling factor in the x-direction (m00). + */ +goog.graphics.AffineTransform.prototype.getScaleX = function() { + return this.m00_; +}; + + +/** + * @return {number} The scaling factor in the y-direction (m11). + */ +goog.graphics.AffineTransform.prototype.getScaleY = function() { + return this.m11_; +}; + + +/** + * @return {number} The translation in the x-direction (m02). + */ +goog.graphics.AffineTransform.prototype.getTranslateX = function() { + return this.m02_; +}; + + +/** + * @return {number} The translation in the y-direction (m12). + */ +goog.graphics.AffineTransform.prototype.getTranslateY = function() { + return this.m12_; +}; + + +/** + * @return {number} The shear factor in the x-direction (m01). + */ +goog.graphics.AffineTransform.prototype.getShearX = function() { + return this.m01_; +}; + + +/** + * @return {number} The shear factor in the y-direction (m10). + */ +goog.graphics.AffineTransform.prototype.getShearY = function() { + return this.m10_; +}; + + +/** + * Concatenates an affine transform to this transform. + * + * @param {!goog.graphics.AffineTransform} tx The transform to concatenate. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.concatenate = function(tx) { + var m0 = this.m00_; + var m1 = this.m01_; + this.m00_ = tx.m00_ * m0 + tx.m10_ * m1; + this.m01_ = tx.m01_ * m0 + tx.m11_ * m1; + this.m02_ += tx.m02_ * m0 + tx.m12_ * m1; + + m0 = this.m10_; + m1 = this.m11_; + this.m10_ = tx.m00_ * m0 + tx.m10_ * m1; + this.m11_ = tx.m01_ * m0 + tx.m11_ * m1; + this.m12_ += tx.m02_ * m0 + tx.m12_ * m1; + return this; +}; + + +/** + * Pre-concatenates an affine transform to this transform. + * + * @param {!goog.graphics.AffineTransform} tx The transform to preconcatenate. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.preConcatenate = function(tx) { + var m0 = this.m00_; + var m1 = this.m10_; + this.m00_ = tx.m00_ * m0 + tx.m01_ * m1; + this.m10_ = tx.m10_ * m0 + tx.m11_ * m1; + + m0 = this.m01_; + m1 = this.m11_; + this.m01_ = tx.m00_ * m0 + tx.m01_ * m1; + this.m11_ = tx.m10_ * m0 + tx.m11_ * m1; + + m0 = this.m02_; + m1 = this.m12_; + this.m02_ = tx.m00_ * m0 + tx.m01_ * m1 + tx.m02_; + this.m12_ = tx.m10_ * m0 + tx.m11_ * m1 + tx.m12_; + return this; +}; + + +/** + * Transforms an array of coordinates by this transform and stores the result + * into a destination array. + * + * @param {!Array.} src The array containing the source points + * as x, y value pairs. + * @param {number} srcOff The offset to the first point to be transformed. + * @param {!Array.} dst The array into which to store the transformed + * point pairs. + * @param {number} dstOff The offset of the location of the first transformed + * point in the destination array. + * @param {number} numPts The number of points to tranform. + */ +goog.graphics.AffineTransform.prototype.transform = function(src, srcOff, dst, + dstOff, numPts) { + var i = srcOff; + var j = dstOff; + var srcEnd = srcOff + 2 * numPts; + while (i < srcEnd) { + var x = src[i++]; + var y = src[i++]; + dst[j++] = x * this.m00_ + y * this.m01_ + this.m02_; + dst[j++] = x * this.m10_ + y * this.m11_ + this.m12_; + } +}; + + +/** + * @return {number} The determinant of this transform. + */ +goog.graphics.AffineTransform.prototype.getDeterminant = function() { + return this.m00_ * this.m11_ - this.m01_ * this.m10_; +}; + + +/** + * Returns whether the transform is invertible. A transform is not invertible + * if the determinant is 0 or any value is non-finite or NaN. + * + * @return {boolean} Whether the transform is invertible. + */ +goog.graphics.AffineTransform.prototype.isInvertible = function() { + var det = this.getDeterminant(); + return goog.math.isFiniteNumber(det) && + goog.math.isFiniteNumber(this.m02_) && + goog.math.isFiniteNumber(this.m12_) && + det != 0; +}; + + +/** + * @return {!goog.graphics.AffineTransform} An AffineTransform object + * representing the inverse transformation. + */ +goog.graphics.AffineTransform.prototype.createInverse = function() { + var det = this.getDeterminant(); + return new goog.graphics.AffineTransform( + this.m11_ / det, + -this.m10_ / det, + -this.m01_ / det, + this.m00_ / det, + (this.m01_ * this.m12_ - this.m11_ * this.m02_) / det, + (this.m10_ * this.m02_ - this.m00_ * this.m12_) / det); +}; + + +/** + * Creates a transform representing a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @return {!goog.graphics.AffineTransform} A transform representing a scaling + * transformation. + */ +goog.graphics.AffineTransform.getScaleInstance = function(sx, sy) { + return new goog.graphics.AffineTransform().setToScale(sx, sy); +}; + + +/** + * Creates a transform representing a translation transformation. + * + * @param {number} dx The distance to translate in the x direction. + * @param {number} dy The distance to translate in the y direction. + * @return {!goog.graphics.AffineTransform} A transform representing a + * translation transformation. + */ +goog.graphics.AffineTransform.getTranslateInstance = function(dx, dy) { + return new goog.graphics.AffineTransform().setToTranslation(dx, dy); +}; + + +/** + * Creates a transform representing a shearing transformation. + * + * @param {number} shx The x-axis shear factor. + * @param {number} shy The y-axis shear factor. + * @return {!goog.graphics.AffineTransform} A transform representing a shearing + * transformation. + */ +goog.graphics.AffineTransform.getShearInstance = function(shx, shy) { + return new goog.graphics.AffineTransform().setToShear(shx, shy); +}; + + +/** + * Creates a transform representing a rotation transformation. + * + * @param {number} theta The angle of rotation measured in radians. + * @param {number} x The x coordinate of the anchor point. + * @param {number} y The y coordinate of the anchor point. + * @return {!goog.graphics.AffineTransform} A transform representing a rotation + * transformation. + */ +goog.graphics.AffineTransform.getRotateInstance = function(theta, x, y) { + return new goog.graphics.AffineTransform().setToRotation(theta, x, y); +}; + + +/** + * Sets this transform to a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.setToScale = function(sx, sy) { + return this.setTransform(sx, 0, 0, sy, 0, 0); +}; + + +goog.graphics.AffineTransform.prototype.isEqualTo = function(t) { + return this.m00_ === t.m00_ && + this.m01_ === t.m01_ && + this.m02_ === t.m02_ && + this.m10_ === t.m10_ && + this.m11_ === t.m11_ && + this.m12_ === t.m12_; + + +}; + +/** + * Sets this transform to a translation transformation. + * + * @param {number} dx The distance to translate in the x direction. + * @param {number} dy The distance to translate in the y direction. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.setToTranslation = function(dx, dy) { + return this.setTransform(1, 0, 0, 1, dx, dy); +}; + + +/** + * Sets this transform to a shearing transformation. + * + * @param {number} shx The x-axis shear factor. + * @param {number} shy The y-axis shear factor. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.setToShear = function(shx, shy) { + return this.setTransform(1, shy, shx, 1, 0, 0); +}; + + +/** + * Sets this transform to a rotation transformation. + * + * @param {number} theta The angle of rotation measured in radians. + * @param {number} x The x coordinate of the anchor point. + * @param {number} y The y coordinate of the anchor point. + * @return {!goog.graphics.AffineTransform} This affine transform. + */ +goog.graphics.AffineTransform.prototype.setToRotation = function(theta, x, y) { + var cos = Math.cos(theta); + var sin = Math.sin(theta); + return this.setTransform(cos, sin, -sin, cos, + x - x * cos + y * sin, y - x * sin - y * cos); +}; + +// END MATRIX STUFF + +/*! + localForage -- Offline Storage, Improved + Version 1.7.3 + https://localforage.github.io/localForage + (c) 2013-2017 Mozilla, Apache License 2.0 +*/ +!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.localforage=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c||a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g=43)}}).catch(function(){return!1})}function n(a){return"boolean"==typeof xa?va.resolve(xa):m(a).then(function(a){return xa=a})}function o(a){var b=ya[a.name],c={};c.promise=new va(function(a,b){c.resolve=a,c.reject=b}),b.deferredOperations.push(c),b.dbReady?b.dbReady=b.dbReady.then(function(){return c.promise}):b.dbReady=c.promise}function p(a){var b=ya[a.name],c=b.deferredOperations.pop();if(c)return c.resolve(),c.promise}function q(a,b){var c=ya[a.name],d=c.deferredOperations.pop();if(d)return d.reject(b),d.promise}function r(a,b){return new va(function(c,d){if(ya[a.name]=ya[a.name]||B(),a.db){if(!b)return c(a.db);o(a),a.db.close()}var e=[a.name];b&&e.push(a.version);var f=ua.open.apply(ua,e);b&&(f.onupgradeneeded=function(b){var c=f.result;try{c.createObjectStore(a.storeName),b.oldVersion<=1&&c.createObjectStore(wa)}catch(c){if("ConstraintError"!==c.name)throw c;console.warn('The database "'+a.name+'" has been upgraded from version '+b.oldVersion+" to version "+b.newVersion+', but the storage "'+a.storeName+'" already exists.')}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){c(f.result),p(a)}})}function s(a){return r(a,!1)}function t(a){return r(a,!0)}function u(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.versiona.db.version;if(d&&(a.version!==b&&console.warn('The database "'+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function v(a){return new va(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function w(a){return g([l(atob(a.data))],{type:a.type})}function x(a){return a&&a.__local_forage_encoded_blob}function y(a){var b=this,c=b._initReady().then(function(){var a=ya[b._dbInfo.name];if(a&&a.dbReady)return a.dbReady});return i(c,a,a),c}function z(a){o(a);for(var b=ya[a.name],c=b.forages,d=0;d0&&(!a.db||"InvalidStateError"===e.name||"NotFoundError"===e.name))return va.resolve().then(function(){if(!a.db||"NotFoundError"===e.name&&!a.db.objectStoreNames.contains(a.storeName)&&a.version<=a.db.version)return a.db&&(a.version=a.db.version+1),t(a)}).then(function(){return z(a).then(function(){A(a,b,c,d-1)})}).catch(c);c(e)}}function B(){return{forages:[],db:null,dbReady:null,deferredOperations:[]}}function C(a){function b(){return va.resolve()}var c=this,d={db:null};if(a)for(var e in a)d[e]=a[e];var f=ya[d.name];f||(f=B(),ya[d.name]=f),f.forages.push(c),c._initReady||(c._initReady=c.ready,c.ready=y);for(var g=[],h=0;h>4,k[i++]=(15&d)<<4|e>>2,k[i++]=(3&e)<<6|63&f;return j}function O(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+=Da[(3&c[b])<<4|c[b+1]>>4],d+=Da[(15&c[b+1])<<2|c[b+2]>>6],d+=Da[63&c[b+2]];return c.length%3==2?d=d.substring(0,d.length-1)+"=":c.length%3==1&&(d=d.substring(0,d.length-2)+"=="),d}function P(a,b){var c="";if(a&&(c=Ua.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===Ua.call(a.buffer))){var d,e=Ga;a instanceof ArrayBuffer?(d=a,e+=Ia):(d=a.buffer,"[object Int8Array]"===c?e+=Ka:"[object Uint8Array]"===c?e+=La:"[object Uint8ClampedArray]"===c?e+=Ma:"[object Int16Array]"===c?e+=Na:"[object Uint16Array]"===c?e+=Pa:"[object Int32Array]"===c?e+=Oa:"[object Uint32Array]"===c?e+=Qa:"[object Float32Array]"===c?e+=Ra:"[object Float64Array]"===c?e+=Sa:b(new Error("Failed to get type for BinaryArray"))),b(e+O(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c=Ea+a.type+"~"+O(this.result);b(Ga+Ja+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(c){console.error("Couldn't convert value into a JSON string: ",a),b(null,c)}}function Q(a){if(a.substring(0,Ha)!==Ga)return JSON.parse(a);var b,c=a.substring(Ta),d=a.substring(Ha,Ta);if(d===Ja&&Fa.test(c)){var e=c.match(Fa);b=e[1],c=c.substring(e[0].length)}var f=N(c);switch(d){case Ia:return f;case Ja:return g([f],{type:b});case Ka:return new Int8Array(f);case La:return new Uint8Array(f);case Ma:return new Uint8ClampedArray(f);case Na:return new Int16Array(f);case Pa:return new Uint16Array(f);case Oa:return new Int32Array(f);case Qa:return new Uint32Array(f);case Ra:return new Float32Array(f);case Sa:return new Float64Array(f);default:throw new Error("Unkown type: "+d)}}function R(a,b,c,d){a.executeSql("CREATE TABLE IF NOT EXISTS "+b.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],c,d)}function S(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new va(function(a,d){try{c.db=openDatabase(c.name,String(c.version),c.description,c.size)}catch(a){return d(a)}c.db.transaction(function(e){R(e,c,function(){b._dbInfo=c,a()},function(a,b){d(b)})},d)});return c.serializer=Va,e}function T(a,b,c,d,e,f){a.executeSql(c,d,e,function(a,g){g.code===g.SYNTAX_ERR?a.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[b.storeName],function(a,h){h.rows.length?f(a,g):R(a,b,function(){a.executeSql(c,d,e,f)},f)},f):f(a,g)},f)}function U(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=e.serializer.deserialize(d)),b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function V(a,b){var c=this,d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT * FROM "+e.storeName,[],function(c,d){for(var f=d.rows,g=f.length,h=0;h0)return void f(W.apply(e,[a,h,c,d-1]));g(b)}})})}).catch(g)});return h(f,c),f}function X(a,b,c){return W.apply(this,[a,b,c,1])}function Y(a,b){var c=this;a=j(a);var d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function Z(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function $(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})}).catch(c)});return h(c,a),c}function _(a,b){var c=this,d=new va(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){T(c,e,"SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})}).catch(d)});return h(d,b),d}function aa(a){var b=this,c=new va(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){T(b,d,"SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e '__WebKitDatabaseInfoTable__'",[],function(c,d){for(var e=[],f=0;f0}function ha(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];return c.keyPrefix=ea(a,b._defaultConfig),ga()?(b._dbInfo=c,c.serializer=Va,va.resolve()):va.reject()}function ia(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=localStorage.length-1;c>=0;c--){var d=localStorage.key(c);0===d.indexOf(a)&&localStorage.removeItem(d)}});return h(c,a),c}function ja(a,b){var c=this;a=j(a);var d=c.ready().then(function(){var b=c._dbInfo,d=localStorage.getItem(b.keyPrefix+a);return d&&(d=b.serializer.deserialize(d)),d});return h(d,b),d}function ka(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo,d=b.keyPrefix,e=d.length,f=localStorage.length,g=1,h=0;h=0;b--){var c=localStorage.key(b);0===c.indexOf(a)&&localStorage.removeItem(c)}}):va.reject("Invalid arguments"),h(d,b),d}function ra(a,b){a[b]=function(){var c=arguments;return a.ready().then(function(){return a[b].apply(a,c)})}}function sa(){for(var a=1;aTheQuickBrownFoxquickly!').get(0); + pixel = pixel || jQuery('').get(0); + if (measureTextNode.parentNode != jQuery('body').get(0)) { + jQuery('body').append(measureTextNode); + } + if (pixel.parentNode != measureTextNode.parentNode) { + + measureTextNode.appendChild(pixel); + } + + + measureTextNode.style.fontFamily = fontFamily; + measureTextNode.style.fontSize = "100px"; + pixel.style.verticalAlign = "text-top"; + measureTextNode.style.display=''; + var top = pixel.offsetTop - measureTextNode.offsetTop + 1; + pixel.style.verticalAlign = "baseline"; + var baseline = pixel.offsetTop - measureTextNode.offsetTop + 1; + + pixel.style.verticalAlign="text-bottom"; + var bottom = pixel.offsetTop - measureTextNode.offsetTop + 1; + + var result = { + ascent: (baseline-top)/100.0, + descent: (bottom-baseline)/100.0 + }; + measureTextNode.style.display='none'; + measureTextCache[fontFamily] = result; + //console.log(fontFamily, result); + return result; + } + + window.measureTextAscent = function(fontFamily) { return window.measureAscentDescent(fontFamily).ascent;} + window.measureTextDescent = function(fontFamily) { return window.measureAscentDescent(fontFamily).descent;} + + + var isMobile = { + Android: function() { + return navigator.userAgent.match(/Android/i); + }, + BlackBerry: function() { + return navigator.userAgent.match(/BlackBerry/i); + }, + iOS: function() { + return navigator.userAgent.match(/iPhone|iPad|iPod/i) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + }, + Opera: function() { + return navigator.userAgent.match(/Opera Mini/i); + }, + Windows: function() { + return navigator.userAgent.match(/IEMobile/i) || navigator.userAgent.match(/WPDesktop/i); + }, + any: function() { + + return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows()); + } + }; + + cn1.isMobile = isMobile; + + + function simulateClick(el) { + var evt; + if (document.createEvent) { + evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + (evt) ? el.dispatchEvent(evt) : (el.click && el.click()); + } + + function capturePhotoWithFileButton(callback, targetWidth, targetHeight){ + var fileBtn = $(''); + var dialog = document.createElement('div'); + dialog.appendChild(fileBtn.get(0)); + dialog.className = 'cn1-capture-dialog'; + + document.querySelector('body').appendChild(dialog); + fileBtn.change(function(event){ + var files = event.target.files; + if (files.length>0){ + + var reader = new FileReader(); + var img = $('').get(0); + $(img).on('load', function(){ + $(dialog).fadeOut(100, function(){ + dialog.parentNode.removeChild(dialog); + }); + var width=targetWidth; + var height=targetHeight; + if (width<!--/g, '><'); + //console.log(pageContent); + + iframe.contentWindow.document.open(); + iframe.contentWindow.document.write(pageContent); + iframe.contentWindow.document.close(); + doc = iframe.contentWindow.document; + var whenReady = function() { + var links = doc.querySelectorAll('a[href]'); + [].forEach.call(links, function(el, index, array) { + el.addEventListener("click", function(evt) { + var absUrl = resolve(el.getAttribute('href'), url, doc); + $(iframe).trigger('cn1load', [$rt_str(absUrl)]); + evt.preventDefault(); + return false; + }); + }); + }; + if (doc.readyState == 'complete') { + whenReady(); + } else { + doc.addEventListener("DOMContentLoaded", whenReady); + + } + + + + }; + +})(); + + +/** + * The Virtual Keyboard Detector + * https://github.com/GillesVermeulen/virtualKeyboardDetector + */ + +window.virtualKeyboardDetector = ( function( window, undefined ) { + + var recentlyFocusedTimeoutDuration = 3000; + + var currentViewportWidth = previousViewportWidth = viewportWidthWithoutVirtualKeyboard = window.innerWidth; + var currentViewportHeight = previousViewportHeight = viewportHeightWithoutVirtualKeyboard = window.innerHeight; + + var virtualKeyboardVisible = false; + var recentlyFocused = false; + var recentlyFocusedTimeout = null; + var validFocusableElements = [ 'INPUT', 'TEXTAREA' ]; + + var subscriptions = {}; + + + /** + * Public functions + */ + + function init ( options ) { + if ( typeof options !== 'undefined' ) { + if ( typeof options.recentlyFocusedTimeoutDuration !== 'undefined' ) recentlyFocusedTimeoutDuration = options.recentlyFocusedTimeoutDuration; + } + + resetViewportSizes(); + initFocusListener(); + initResizeListener(); + } + + function isVirtualKeyboardVisible () { + return virtualKeyboardVisible; + } + + function getVirtualKeyboardSize () { + if ( !virtualKeyboardVisible ) return false; + + return { + width: currentViewportWidth, + height: viewportHeightWithoutVirtualKeyboard - currentViewportHeight + }; + } + + // Subscribe + function on ( eventName, fn ) { + if ( typeof subscriptions[eventName] === 'undefined' ) subscriptions[eventName] = []; + + subscriptions[eventName].push(fn); + } + + // Unsubscribe + function off ( eventName, fn ) { + if (typeof subscriptions[eventName] === 'undefined' ) return; + + if (typeof fn === 'undefined') { + subscriptions[eventName] = []; + } else { + var i = subscriptions[eventName].length; + while ( i-- ) { + if ( subscriptions[eventName][i] == fn ) subscriptions[eventName].splice(i, 1); + } + } + } + + // Publish + function trigger ( eventName, args ) { + for ( i in subscriptions[eventName] ) { + if ( typeof subscriptions[eventName][i] === 'function' ) subscriptions[eventName][i]( args ); + } + } + + + /** + * Private functions + */ + + // Reset all sizes. We presume the virtual keyboard is not visible at this stage. + // We call this function on initialisation, so make sure you initialise the virtualKeyBoardListener at a moment when the virtual keyboard is likely to be invisible. + function resetViewportSizes () { + currentViewportWidth = previousViewportWidth = viewportWidthWithoutVirtualKeyboard = window.innerWidth; + currentViewportHeight = previousViewportHeight = viewportHeightWithoutVirtualKeyboard = window.innerHeight; + } + + // Initialise the listener that checks for focus events in the whole document. This way we can also handle dynamically added focusable elements. + function initFocusListener () { + document.addEventListener( 'focus', documentFocusHandler, true ); + } + + // Handle the document focus event. We check if the target was a valid focusable element. + function documentFocusHandler (e) { + if (typeof e.target !== 'undefined' && typeof e.target.nodeName !== 'undefined') { + if (validFocusableElements.indexOf(e.target.nodeName) != -1) elementFocusHandler(e); + } + } + + // Handle the case when a valid focusable element is focused. We flag that a valid element was recently focused. This flag expires after recentlyFocusedTimeoutDuration. + function elementFocusHandler (e) { + if ( recentlyFocusedTimeout != null ) { + window.clearTimeout( recentlyFocusedTimeout ); + recentlyFocusedTimeout = null; + } + + recentlyFocused = true; + + recentlyFocusedTimeout = window.setTimeout( expireRecentlyFocused, recentlyFocusedTimeoutDuration ); + } + + function expireRecentlyFocused () { + recentlyFocused = false; + } + + function initResizeListener () { + window.addEventListener( 'resize', resizeHandler ); + } + + function resizeHandler () { + currentViewportWidth = window.innerWidth; + currentViewportHeight = window.innerHeight; + + // If the virtual keyboard is tought to be visible, but the viewport height returns to the value before keyboard was visible, we presume the keyboard was hidden. + if ( virtualKeyboardVisible && currentViewportWidth == previousViewportWidth && currentViewportHeight >= viewportHeightWithoutVirtualKeyboard ) { + virtualKeyboardHiddenHandler(); + } + + // If the width of the viewport is changed, it's hard to tell wether virtual keyboard is still visible, so we make sure it's not. + if ( currentViewportWidth != previousViewportWidth ) { + if ( 'activeElement' in document ) + document.activeElement.blur(); + virtualKeyboardHiddenHandler(); + } + + // If recently focused and viewport height is smaller then previous height, we presume that the virtual keyboard has appeared. + if ( !virtualKeyboardVisible && recentlyFocused && currentViewportWidth == previousViewportWidth && currentViewportHeight < previousViewportHeight ) { + virtualKeyboardVisibleHandler(); + } + + // If the keyboard is presumed not visible, we save the current measurements as values before keyboard was shown. + if ( virtualKeyboardVisible == false ) { + viewportWidthWithoutVirtualKeyboard = currentViewportWidth; + viewportHeightWithoutVirtualKeyboard = currentViewportHeight; + } + + previousViewportWidth = currentViewportWidth; + previousViewportHeight = currentViewportHeight; + } + + function virtualKeyboardVisibleHandler () { + virtualKeyboardVisible = true; + + var eventData = { + virtualKeyboardVisible: virtualKeyboardVisible, + sizes: getSizesData() + }; + + trigger( 'virtualKeyboardVisible', eventData ); + } + + function virtualKeyboardHiddenHandler () { + virtualKeyboardVisible = false; + + var eventData = { + virtualKeyboardVisible: virtualKeyboardVisible, + sizes: getSizesData() + }; + + trigger( 'virtualKeyboardHidden', eventData ); + } + + function getSizesData () { + return { + viewportWithoutVirtualKeyboard: { + width: viewportWidthWithoutVirtualKeyboard, + height: viewportHeightWithoutVirtualKeyboard + }, + currentViewport: { + width: currentViewportWidth, + height: currentViewportHeight + }, + virtualKeyboard: { + width: currentViewportWidth, + height: viewportHeightWithoutVirtualKeyboard - currentViewportHeight + } + }; + } + + // Make public functions available + return { + init: init, + isVirtualKeyboardVisible: isVirtualKeyboardVisible, + getVirtualKeyboardSize: getVirtualKeyboardSize, + on: on, + addEventListener: on, + subscribe: on, + off: off, + removeEventListener: off, + unsubscribe: off, + trigger: trigger, + publish: trigger, + dispatchEvent: trigger + }; + +} )( window ); + + +(function() { + window.cn1GetImageOrientation = getOrientation; + window.cn1ResetImageOrientation = resetImageOrientation; + + function resetImageOrientation(srcBlob, srcOrientation, callback) { + var img = new Image(); + + img.onload = function() { + var width = img.width, + height = img.height, + canvas = document.createElement('canvas'), + ctx = canvas.getContext("2d"); + + // set proper canvas dimensions before transform & export + if (4 < srcOrientation && srcOrientation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + // transform context before drawing image + switch (srcOrientation) { + case 2: ctx.transform(-1, 0, 0, 1, width, 0); break; + case 3: ctx.transform(-1, 0, 0, -1, width, height); break; + case 4: ctx.transform(1, 0, 0, -1, 0, height); break; + case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; + case 6: ctx.transform(0, 1, -1, 0, height, 0); break; + case 7: ctx.transform(0, -1, -1, 0, height, width); break; + case 8: ctx.transform(0, -1, 1, 0, 0, width); break; + default: break; + } + + // draw image + ctx.drawImage(img, 0, 0); + + // export blob + + + callback(canvas); + }; + + img.src = URL.createObjectURL(srcBlob); + }; + + + function getOrientation(file, callback) { + var reader = new FileReader(); + reader.onload = function(e) { + + var view = new DataView(e.target.result); + if (view.getUint16(0, false) != 0xFFD8) + { + return callback(-2); + } + var length = view.byteLength, offset = 2; + while (offset < length) + { + if (view.getUint16(offset+2, false) <= 8) return callback(-1); + var marker = view.getUint16(offset, false); + offset += 2; + if (marker == 0xFFE1) + { + if (view.getUint32(offset += 2, false) != 0x45786966) + { + return callback(-1); + } + + var little = view.getUint16(offset += 6, false) == 0x4949; + offset += view.getUint32(offset + 4, little); + var tags = view.getUint16(offset, little); + offset += 2; + for (var i = 0; i < tags; i++) + { + if (view.getUint16(offset + (i * 12), little) == 0x0112) + { + return callback(view.getUint16(offset + (i * 12) + 8, little)); + } + } + } + else if ((marker & 0xFF00) != 0xFF00) + { + break; + } + else + { + offset += view.getUint16(offset, false); + } + } + return callback(-1); + }; + reader.readAsArrayBuffer(file); + } + +})(); + +(function() { + if (!window.MediaRecorder) { + var AudioContext = window.AudioContext || window.webkitAudioContext + + function createWorker (fn) { + var js = fn + .toString() + .replace(/^function\s*\(\)\s*{/, '') + .replace(/}$/, '') + var blob = new Blob([js]) + return new Worker(URL.createObjectURL(blob)) + } + + function error (method) { + var event = new Event('error') + event.data = new Error('Wrong state for ' + method) + return event + } + + var context, processor + + /** + * Audio Recorder with MediaRecorder API. + * + * @param {MediaStream} stream The audio stream to record. + * + * @example + * navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { + * var recorder = new MediaRecorder(stream) + * }) + * + * @class + */ + function MediaRecorder (stream) { + /** + * The `MediaStream` passed into the constructor. + * @type {MediaStream} + */ + this.stream = stream + + /** + * The current state of recording process. + * @type {"inactive"|"recording"|"paused"} + */ + this.state = 'inactive' + + this.em = document.createDocumentFragment() + this.encoder = createWorker(MediaRecorder.encoder) + + var recorder = this + this.encoder.addEventListener('message', function (e) { + var event = new Event('dataavailable') + event.data = new Blob([e.data], { type: recorder.mimeType }) + recorder.em.dispatchEvent(event) + if (recorder.state === 'inactive') { + recorder.em.dispatchEvent(new Event('stop')) + } + }) + } + + MediaRecorder.prototype = { + /** + * The MIME type that is being used for recording. + * @type {string} + */ + mimeType: 'audio/wav', + + /** + * Begins recording media. + * + * @param {number} [timeslice] The milliseconds to record into each `Blob`. + * If this parameter isn’t included, single `Blob` + * will be recorded. + * + * @return {undefined} + * + * @example + * recordButton.addEventListener('click', function () { + * recorder.start() + * }) + */ + start: function start (timeslice) { + if (this.state !== 'inactive') { + return this.em.dispatchEvent(error('start')) + } + + this.state = 'recording' + + if (!context) { + context = new AudioContext() + } + this.clone = this.stream.clone() + var input = context.createMediaStreamSource(this.clone) + + if (!processor) { + processor = context.createScriptProcessor(2048, 1, 1) + } + + var recorder = this + processor.onaudioprocess = function (e) { + if (recorder.state === 'recording') { + recorder.encoder.postMessage([ + 'encode', e.inputBuffer.getChannelData(0) + ]) + } + } + + input.connect(processor) + processor.connect(context.destination) + + this.em.dispatchEvent(new Event('start')) + + if (timeslice) { + this.slicing = setInterval(function () { + if (recorder.state === 'recording') recorder.requestData() + }, timeslice) + } + + return undefined + }, + + /** + * Stop media capture and raise `dataavailable` event with recorded data. + * + * @return {undefined} + * + * @example + * finishButton.addEventListener('click', function () { + * recorder.stop() + * }) + */ + stop: function stop () { + if (this.state === 'inactive') { + return this.em.dispatchEvent(error('stop')) + } + + this.requestData() + this.state = 'inactive' + this.clone.getTracks().forEach(function (track) { + track.stop() + }) + return clearInterval(this.slicing) + }, + + /** + * Pauses recording of media streams. + * + * @return {undefined} + * + * @example + * pauseButton.addEventListener('click', function () { + * recorder.pause() + * }) + */ + pause: function pause () { + if (this.state !== 'recording') { + return this.em.dispatchEvent(error('pause')) + } + + this.state = 'paused' + return this.em.dispatchEvent(new Event('pause')) + }, + + /** + * Resumes media recording when it has been previously paused. + * + * @return {undefined} + * + * @example + * resumeButton.addEventListener('click', function () { + * recorder.resume() + * }) + */ + resume: function resume () { + if (this.state !== 'paused') { + return this.em.dispatchEvent(error('resume')) + } + + this.state = 'recording' + return this.em.dispatchEvent(new Event('resume')) + }, + + /** + * Raise a `dataavailable` event containing the captured media. + * + * @return {undefined} + * + * @example + * this.on('nextData', function () { + * recorder.requestData() + * }) + */ + requestData: function requestData () { + if (this.state === 'inactive') { + return this.em.dispatchEvent(error('requestData')) + } + + return this.encoder.postMessage(['dump', context.sampleRate]) + }, + + /** + * Add listener for specified event type. + * + * @param {"start"|"stop"|"pause"|"resume"|"dataavailable"|"error"} + * type Event type. + * @param {function} listener The listener function. + * + * @return {undefined} + * + * @example + * recorder.addEventListener('dataavailable', function (e) { + * audio.src = URL.createObjectURL(e.data) + * }) + */ + addEventListener: function addEventListener () { + this.em.addEventListener.apply(this.em, arguments) + }, + + /** + * Remove event listener. + * + * @param {"start"|"stop"|"pause"|"resume"|"dataavailable"|"error"} + * type Event type. + * @param {function} listener The same function used in `addEventListener`. + * + * @return {undefined} + */ + removeEventListener: function removeEventListener () { + this.em.removeEventListener.apply(this.em, arguments) + }, + + /** + * Calls each of the listeners registered for a given event. + * + * @param {Event} event The event object. + * + * @return {boolean} Is event was no canceled by any listener. + */ + dispatchEvent: function dispatchEvent () { + this.em.dispatchEvent.apply(this.em, arguments) + } + } + + /** + * Returns `true` if the MIME type specified is one the polyfill can record. + * + * This polyfill supports only `audio/wav`. + * + * @param {string} mimeType The mimeType to check. + * + * @return {boolean} `true` on `audio/wav` MIME type. + */ + MediaRecorder.isTypeSupported = function isTypeSupported (mimeType) { + return /audio\/wave?/.test(mimeType) + } + + /** + * `true` if MediaRecorder can not be polyfilled in the current browser. + * @type {boolean} + * + * @example + * if (MediaRecorder.notSupported) { + * showWarning('Audio recording is not supported in this browser') + * } + */ + MediaRecorder.notSupported = !navigator.mediaDevices || !AudioContext + + /** + * Converts RAW audio buffer to compressed audio files. + * It will be loaded to Web Worker. + * By default, WAVE encoder will be used. + * @type {function} + * + * @example + * MediaRecorder.prototype.mimeType = 'audio/ogg' + * MediaRecorder.encoder = oggEncoder + */ + MediaRecorder.encoder = function () { + var BYTES_PER_SAMPLE = 2 + + var recorded = [] + + function encode (buffer) { + var length = buffer.length + var data = new Uint8Array(length * BYTES_PER_SAMPLE) + for (var i = 0; i < length; i++) { + var index = i * BYTES_PER_SAMPLE + var sample = buffer[i] + if (sample > 1) { + sample = 1 + } else if (sample < -1) { + sample = -1 + } + sample = sample * 32768 + data[index] = sample + data[index + 1] = sample >> 8 + } + recorded.push(data) + } + + function dump (sampleRate) { + var bufferLength = recorded.length ? recorded[0].length : 0 + var length = recorded.length * bufferLength + var wav = new Uint8Array(44 + length) + var view = new DataView(wav.buffer) + + // RIFF identifier 'RIFF' + view.setUint32(0, 1380533830, false) + // file length minus RIFF identifier length and file description length + view.setUint32(4, 36 + length, true) + // RIFF type 'WAVE' + view.setUint32(8, 1463899717, false) + // format chunk identifier 'fmt ' + view.setUint32(12, 1718449184, false) + // format chunk length + view.setUint32(16, 16, true) + // sample format (raw) + view.setUint16(20, 1, true) + // channel count + view.setUint16(22, 1, true) + // sample rate + view.setUint32(24, sampleRate, true) + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * BYTES_PER_SAMPLE, true) + // block align (channel count * bytes per sample) + view.setUint16(32, BYTES_PER_SAMPLE, true) + // bits per sample + view.setUint16(34, 8 * BYTES_PER_SAMPLE, true) + // data chunk identifier 'data' + view.setUint32(36, 1684108385, false) + // data chunk length + view.setUint32(40, length, true) + + for (var i = 0; i < recorded.length; i++) { + wav.set(recorded[i], i * bufferLength + 44) + } + + recorded = [] + postMessage(wav.buffer, [wav.buffer]) + } + + onmessage = function (e) { + if (e.data[0] === 'encode') { + encode(e.data[1]) + } else { + dump(e.data[1]) + } + } + } + + window.MediaRecorder = MediaRecorder; + } +})(); + +(function() { + var isEdge = window.navigator.userAgent.indexOf("Edge/") > -1; + var isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || !!navigator.userAgent.match(/Trident.*rv\:11\./); + var isSafari = _isSafari(); + function _isSafari() { + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf('safari') != -1) { + if (ua.indexOf('chrome') > -1) { + //return false; + } else { + return true; + } + } + return false; + } + + function isIOS() { + + return (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + } + + function isIPad() { + return (/ipad/.test(window.location.search)); + //return (/iPad/.test(navigator.userAgent) && !window.MSStream) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); + } + function isIOS13() { + return isIOS() && "download" in document.createElement("a") + } + + function createAudioContext(desiredSampleRate) { + var AudioCtor = window.AudioContext || window.webkitAudioContext + var supportedConstraints = navigator.mediaDevices.getSupportedConstraints ? + navigator.mediaDevices.getSupportedConstraints() : {}; + + if (isIOS() || !supportedConstraints.sampleRate) { + + var context = new AudioCtor(); + var buffer = context.createBuffer(1, 1, 44100) + var dummy = context.createBufferSource() + dummy.buffer = buffer + dummy.connect(context.destination) + dummy.start(0) + dummy.disconnect() + context.close(); + context = new AudioCtor(); + return context; + } else { + return new AudioCtor({sampleRate: desiredSampleRate}); + } + } + + + function unlockAudioClip(audio) { + + try { + audio.setAttribute('data-cn1-unlocked', 'true'); + console.log('Unlocking audio ', audio); + var testPlay = audio.play(); + + + if (testPlay && typeof Promise !== 'undefined' && (testPlay instanceof Promise || typeof testPlay.then === 'function')) { + testPlay.catch(function(err) { + if (err.name=='NotAllowedError' || err.name == 'AbortError') { + var index = (window._unlockedAudioPool || []).indexOf(audio); + if (index >= 0) { + window._unlockedAudioPool.splice(index, 1); + } + } + }); + } + + } catch (err) { + + } + + } + var unlockAudio = function() { + try { + window.cn1UnlockingClips = true; + document.removeEventListener('touchstart', unlockAudio, true); + document.removeEventListener('touchend', unlockAudio, true); + document.removeEventListener('click', unlockAudio, true); + window.removeEventListener('installbacksidehooks', unlockAudio, true); + + for (var i=0; i<5; i++) { + unlockAudioClip(new Audio()); + } + + } finally { + window.cn1UnlockingClips = false; + } + + }; + document.addEventListener('touchstart', unlockAudio, true); + document.addEventListener('touchend', unlockAudio, true); + document.addEventListener('click', unlockAudio, true); + window.addEventListener('installbacksidehooks', unlockAudio, true); + + var hidden, visibilityChange; + if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support + hidden = "hidden"; + visibilityChange = "visibilitychange"; + } else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } + + + + function AudioUnit(config) { + config = config || {}; + var healthCheckHandle; + var origChannelCount=-1; + var origSampleRate=-1; + var paused; + var audioCtx; + var audioInput; + var audioNode; + var bufferSize = config.bufferSize || 4096; + var stream; + var recording = false; + var SAMPLE_RATE = config.sampleRate || 44100; + var ENABLE_SAMPLERATE = true; // Whether to use libsamplerate if it is availalble + var SAMPLE_SIZE = config.sampleSize || 16; + var audioChannels = config.audioChannels || 1; + var onComplete = config.onComplete || function(str){}; + var _onError = config.onError || function(e){console.log(e);}; + var onError = function(e){ + console.log("Firing onError callback ", e); + _onError(e); + } + var onAudioProcess = config.onAudioProcess || function(sampleRate, numChannels, floatSamples){}; + var _onRecord = config.onRecord || function(numChannels, sampleRate){}; + var onRecord = function(numChannels, sampleRate) { + console.log("firing onRecord callback", numChannels, sampleRate); + _onRecord(numChannels, sampleRate); + }; + var pendingGetUserMedia = false; + console.log("AudioUnit config", config); + function interleave(e) { + if (audioChannels === 1) { + return e.inputBuffer.getChannelData(0); + } else if (audioChannels === 2) { + var leftChannel = e.inputBuffer.getChannelData(0); + var rightChannel = e.inputBuffer.getChannelData(1); + var length = leftChannel.length + rightChannel.length; + var result = new Float32Array(length); + var inputIndex = 0; + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; + + } else { + throw new Error("Unsupported channel count: "+audioChannels+". Supports only 1 or 2"); + } + + } + + + /** + * Checks if the media stream is currently active. + */ + function isPaused() { + var active = false; + if (stream) { + + stream.getTracks().forEach(function(track) { + if (track.enabled) { + active = true; + } + }); + + + } + return active; + } + + function resume(notAllowedCallback, blockRecordCallback) { + try { + console.log('RECORDER|resume'); + console.log("in resume()", stream); + paused = false; + recording = true; + if (stream) { + stream.getTracks().forEach(function(track) { + console.log("enabling track", track); + track.enabled = true; + }); + } + + + if (audioCtx == null) { + console.log("Creating audio context"); + audioCtx = createAudioContext(SAMPLE_RATE); + audioInput = audioCtx.createMediaStreamSource(stream); + console.log("Audio context created"); + if (audioCtx.createJavaScriptNode) { + audioNode = audioCtx.createJavaScriptNode(bufferSize, audioInput.channelCount, audioCtx.destination.channelCount); + } else if (audioCtx.createScriptProcessor) { + audioNode = audioCtx.createScriptProcessor(bufferSize, audioInput.channelCount, audioCtx.destination.channelCount); + } else { + console.log("Failed to create audio context"); + recording = false; + onError("Failed to create audio context"); + return; + } + + } + + console.log("recording now"); + // Hook up the scriptNode to the mic + + + audioInput.connect(audioNode); + //do I need to move this + audioNode.connect(audioCtx.destination); + + console.log("about to fire onRecord callback"); + + setupAudioProcessing(audioNode, audioCtx, notAllowedCallback, blockRecordCallback); + + + + + console.log("Finished resume()"); + } catch (e) { + recording = false; + console.log("ERROR in resume", e); + onError(e+""); + } + + } + + + + function stop() { + + pause(); + if (audioInput) { + if (audioInput.disconnect) { + audioInput.disconnect(); + } + audioInput = null; + } + if (audioNode) { + if (audioNode.disconnect) { + audioNode.disconnect(); + } + audioNode = null; + } + if (audioCtx) { + if (audioCtx.close) { + audioCtx.close(); + + } + audioCtx = null; + } + + if (stream) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + if (stream.close) { + stream.close(); + } + stream = null; + } + + + onComplete(''); + + + }; + + + + function pause() { + console.log("In pause()", stream); + paused = true; + recording = false; + if (healthCheckHandle) { + clearInterval(healthCheckHandle); + healthCheckHandle = null; + } + if (stream) { + stream.getTracks().forEach(function(track) { + console.log("Disabling track", track); + track.enabled = false; + }); + + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + if (audioNode) { + + audioNode.disconnect(); + audioNode = null; + } + + if (audioCtx) { + + var thisCtx = audioCtx; + audioCtx.close().then(function() { + //console.log("Finished closing context ", thisCtx); + }); + audioCtx = null; + } + + + if (isSafari || (isIOS() && !isIPad())) { + if (stream) { + stream.getTracks().forEach(function(track) { + //console.log("Stopping track", track); + track.stop(); + }); + if (stream.close) { + stream.close(); + } + if (stream.stop) { + stream.stop(); + } + stream = null; + } + } + } + + + } + + function setupAudioProcessing(audioNode, audioCtx, notAllowedCallback, blockRecordCallback) { + var pipe = {success:false}; + var requestComplete = false; + healthCheckHandle = setInterval(function() { + if (!pipe.success) { + pipe.success = false; + console.log("No audio processed since last poll"); + } + if (/*!pipe.success || */false && audioCtx && audioCtx.state === 'interrupted') { + + console.log("Stream interrupted. Trying to resume"); + pause(); + var naCallback = function() { + if (!window.confirm('Continue recording?')) { + stop(); + } else { + pause(); + record(naCallback, true); + } + }; + record(naCallback, true); + + } + }, 500); + + if (!blockRecordCallback) { + onRecord(audioChannels, Math.floor(audioCtx.sampleRate)); + } + + registerAudioProcessing(audioNode, audioCtx, pipe); + + + } + + function registerAudioProcessing(audioNode, audioCtx, pipe) { + audioNode.onaudioprocess = function(e) { + if (pipe && !pipe.success) { + pipe.success = true; + } + try { + var floatSamples = interleave(e); + //console.log("float samples "+floatSamples); + var sr = audioCtx.sampleRate; + if (ENABLE_SAMPLERATE) { + if (sr != SAMPLE_RATE && 'Samplerate' in window) { + var resampler = new Samplerate({type: Samplerate.LINEAR}); + + var result = resampler.process({ + channels: audioChannels, + data: floatSamples, // buffer is a Float32Aray or a Int16Array + ratio: parseFloat(SAMPLE_RATE)/parseFloat(sr), + last: false + }); + + var converted = result.data; // same type as buffer above + var used = result.used; // input samples effectively used + + // Optional: + //converter.setRatio(2.3); + //converter.reset(); + + // Close: + resampler.close(); + floatSamples = converted; + sr = SAMPLE_RATE; + } + } + + if (isEdge || isIE) { + // Not sure why this is necessary, but in Edge and IE, + // the length of typed arrays is zero by the time it is processed + // on the java audio processing thread. + // We need to convert it to a regular array + // so that the data gets retained. Must be a bug in IE/Edge, + // but couldn't find it reported anywhere. + floatSamples = Array.from(floatSamples); + } + + onAudioProcess(Math.floor(sr), audioChannels, floatSamples); + } catch (e) { + onError(e+""); + + } + + }; + } + + function promptAsync(msg) { + return new Promise(function(resolve, reject) { + var dialog = $('
Grant access to microphone?
') + .css({ + position:'fixed', + top: '40%', + width: '100px', + 'background-color' : 'white' + }) + .appendTo($('body')); + $('.ok-btn', dialog).on('click', function() { + $(dialog).remove(); + resolve(true); + }); + $('.cancel-btn', dialog).on('click', function() { + $(dialog).remove(); + resolve(false); + }); + }); + } + + + var failedMEICheck; + function record(notAllowedCallback, blockRecordCallback, allowed) { + if (recording) { + console.log("Calling onRecord because recording"); + onRecord(audioChannels, audioCtx ? Math.floor(audioCtx.sampleRate) : 0); + return; + } + if (pendingGetUserMedia) { + // If there is a pending getUser media, then + // interested parties will receive the onRecord or onError + // callbacks from that request. + return; + } + + + failedMEICheck = false; + + if ((isIOS() || isSafari) && !allowed) { + cn1RunPrivileged(function() { + record(notAllowedCallback, blockRecordCallback, true); + }); + + return; + + + } + + + paused = false; + + if (stream && (!isIOS() || isIPad())) { + if (isIOS()) { + // On iOS we cannot reuse the stream, and it is problematic + // to create it new every time, so we workaround it by cloning + // the stream and removing the tracks from the old stream. + // Thanks Chad Phillips (https://webrtchacks.com/guide-to-safari-webrtc/) + var tmp = stream.clone(); + var audioTracks = stream.getAudioTracks(); + for (var i=0, len = audioTracks.length; i < len; i++) { + stream.removeTrack(audioTracks[i]); + } + stream = tmp; + } + resume(notAllowedCallback, blockRecordCallback); + return; + } + + if (stream) { + stream.getTracks().forEach(function(track) { + console.log("Disabling track", track); + track.stop(); + }); + if (stream.stop) { + stream.stop(); + } + if (stream.close) { + stream.close(); + } + stream = null; + } + + pendingGetUserMedia = true; + + // On iOS it is extremely important to have echoCancellation OFF + // Otherwise you'll get distortion in the recording if ANY audio clips have been played in the + // app. + // On Android, it is extremely important to NOT have echo cancellation off + // With echo cancellation set to false, the mic is VERY soft + var audioConstraints = isIOS() ? {echoCancellation:false} : {}; + var supportedConstraints = {}; + if (!isIOS()) { + if (navigator.mediaDevices.getSupportedConstraints) { + supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + } + + if (supportedConstraints.sampleSize) { + audioConstraints.sampleSize = {ideal: SAMPLE_SIZE}; + } + if (supportedConstraints.sampleRate) { + audioConstraints.sampleRate = {ideal: SAMPLE_RATE}; + } + if (supportedConstraints.audioChannels) { + audioConstraints.channelCount = {ideal: audioChannels}; + } + if (supportedConstraints.autoGainControl) { + audioConstraints.autoGainControl = true; // For Android + } + } + //audioConstraints.autoGainControl=true; + //IMPORTANT!!: On iPad you MUST create the audioCtx and audioNode + // and connect the audioNode to the audioCtx.destination BEFORE + // getUserMedia(). If you do it after the callback, you get all kinds + // of crazy results related to the audio not starting in direct response to a + // user action. + // Symptoms include: + // 1. Audio context randomly gets kicked into the "interrupted" state. + // 2. onaudioprocess works correctly for the first few seconds, but then + // the channel data starts coming in all zeroes. + // 3. Microphone quality gets very poor and distored. + // 4. Audio playback becomes soft. + audioCtx = createAudioContext(SAMPLE_RATE); + audioNode = (audioCtx.createJavascriptNode||audioCtx.createScriptProcessor).call(audioCtx, bufferSize, audioChannels, audioChannels); + audioNode.connect(audioCtx.destination); + + console.log("About to do getUserMedia"); + + + function onStream(_stream, privileged) { + if ((isIOS() || isSafari) && !privileged) { + cn1RunPrivileged(function() { + onStream(_stream, true); + }); + return; + } + /* + if (!isIOS() && isSafari && !window.cn1MicrophoneAccessGranted) { + // Unbelievable!! The standard MEI tests fail in Safari on Mac. + // I.e. It will pass the MEI check even if it should fail. + // We fill force a fail on the MEI check the first time we request + // microphone access on safari because if we don't, it won't pick up + // any audio the first time around. + failedMEICheck = true; + } + */ + window.cn1MicrophoneAccessGranted = true; + console.log("In getUserMedia callback"); + pendingGetUserMedia = false; + + stream = _stream; + if (failedMEICheck) { + failedMEICheck = false; + console.log("Failed MEI Check, so we're going back to basics"); + pause(); + notAllowedCallback(); + + return; + } + if (paused) { + console.log("recorder paused before we got user media"); + try { + pause(); + } catch (ex) { + console.log("pause() threw exception", ex); + } + + onError("Record was paused"); + return; + } + try { + + if (origChannelCount < 0) { + origChannelCount = audioCtx.destination.channelCount; + } + if (origSampleRate < 0) { + origSampleRate = audioCtx.sampleRate; + } + audioInput = audioCtx.createMediaStreamSource(stream); + if (!audioCtx.createJavascriptNode && !audioCtx.createScriptProcessor) { + console.log("Failed to create audio context"); + onError("Failed to create audio context"); + return; + } + + console.log("recording now"); + + audioInput.connect(audioNode); + setupAudioProcessing(audioNode, audioCtx, notAllowedCallback, blockRecordCallback); + if (paused) { + console.log("Already paused by the time we got permission"); + pause(); + onError("Record was paused."); + } else { + recording = true; + + } + + } catch (e) { + if (stream) { + stream.getTracks().forEach(function(track) { + track.enabled = false; + }); + } + if (stream) { + if (stream.close) { + stream.close(); + } + stream = null; + } + if (audioInput) { + if (audioInput.disconnect) { + audioInput.disconnect(); + } + audioInput = null; + } + if (audioNode) { + if (audioNode.disconnect) { + audioNode.disconnect(); + } + audioNode = null; + + } + if (audioCtx) { + if (audioCtx.close) { + audioCtx.close(); + } + audioCtx = null; + } + + console.log("Failed to create audio context " + e); + onError(e+""); + return; + } + } + + navigator.mediaDevices.getUserMedia({ + //audio: { + //echoCancellation: true, + //autoGainControl: true, + //channelCount: audioChannels, + //sampleRate:SAMPLE_RATE, + //sampleSize: SAMPLE_SIZE, + //volume: 1.0 + //} + audio: audioConstraints, + video : false + + }).then(onStream).catch(function (err) { + pendingGetUserMedia = false; + + if ((err.name == 'NotAllowedError' || err.name == 'AbortError') && notAllowedCallback) { + notAllowedCallback(); + return; + } + console.log("Failed to get user media " + err); + onError(err+""); + }); + + console.log("After getUserMedia call"); + ; + } + + function isRecording() { + return recording; + } + + this.record = record; + this.pause = pause; + this.stop = stop; + this.isRecording = isRecording; + + } + + + + window.cn1CreateSilentAudio = createSilentAudio; + function createSilentAudio (time, freq = 44100){ + console.log("Creating silent audio", time, freq); + const length = time * freq; + const AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext; + if(! AudioContext ){ + console.log("No Audio Context") + } + const context = new AudioContext(); + const audioFile = context.createBuffer(1, length, freq); + return URL.createObjectURL(bufferToWave(audioFile, length)); + } + + function bufferToWave(abuffer, len) { + let numOfChan = abuffer.numberOfChannels, + length = len * numOfChan * 2 + 44, + buffer = new ArrayBuffer(length), + view = new DataView(buffer), + channels = [], i, sample, + offset = 0, + pos = 0; + + // write WAVE header + setUint32(0x46464952); + setUint32(length - 8); + setUint32(0x45564157); + + setUint32(0x20746d66); + setUint32(16); + setUint16(1); + setUint16(numOfChan); + setUint32(abuffer.sampleRate); + setUint32(abuffer.sampleRate * 2 * numOfChan); + setUint16(numOfChan * 2); + setUint16(16); + + setUint32(0x61746164); + setUint32(length - pos - 4); + + // write interleaved data + for(i = 0; i < abuffer.numberOfChannels; i++) + channels.push(abuffer.getChannelData(i)); + + while(pos < length) { + for(i = 0; i < numOfChan; i++) { // interleave channels + sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp + sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale to 16-bit signed int + view.setInt16(pos, sample, true); // write 16-bit sample + pos += 2; + } + offset++ // next source sample + } + + // create Blob + return new Blob([buffer], {type: "audio/wav"}); + + function setUint16(data) { + view.setUint16(pos, data, true); + pos += 2; + } + + function setUint32(data) { + view.setUint32(pos, data, true); + pos += 4; + } + } + + + + window.CN1AudioUnit = AudioUnit; +})(); + +(function() { + function AudioRecorder(config) { + config = config || {}; + var recorder = null; + var recordedChunks = null; + var TIME_SLICE = config.timeSlice || 0; + var savePath = config.savePath || 'file:///tempRecording.wav'; + var fireOnComplete = config.onComplete || function(path){}; + var fireOnError = config.onError || function(error){} + var fireOnRecord = config.onRecord || function(audioTracks, sampleRate){} + var stopping=false; + + + function record(notAllowedCallback) { + console.log("In record()"); + if (recorder != null) { + recorder.resume(); + return; + } + + recordedChunks = []; + console.log("RECORDER|getUserMedia audioRecorder"); + navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) { + recorder = new MediaRecorder(stream) + + // Set record to