mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +00:00
Partial conversion to new UI paradigm and manager.
This commit is contained in:
@@ -40,11 +40,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea id="eventTarget"></textarea>
|
||||
|
||||
<!-- On-screen magnifier -->
|
||||
<div id="magnifier">
|
||||
<textarea id="eventTarget"></textarea>
|
||||
<canvas id="magnifier-display"></canvas>
|
||||
<div id="magnifier-background">
|
||||
<div id="magnifier">
|
||||
<canvas id="magnifier-display"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pan-overlay"></div>
|
||||
|
||||
<!-- On-screen keyboard -->
|
||||
<div id="keyboardContainer"></div>
|
||||
@@ -78,6 +83,8 @@
|
||||
<script type="text/javascript" src="scripts/session.js"></script>
|
||||
<script type="text/javascript" src="scripts/history.js"></script>
|
||||
<script type="text/javascript" src="scripts/interface.js"></script>
|
||||
<script type="text/javascript" src="scripts/guac-ui.js"></script>
|
||||
<script type="text/javascript" src="scripts/client-ui.js"></script>
|
||||
|
||||
<!-- Init -->
|
||||
<script type="text/javascript"> /* <![CDATA[ */
|
||||
|
246
guacamole/src/main/webapp/scripts/client-ui.js
Normal file
246
guacamole/src/main/webapp/scripts/client-ui.js
Normal file
@@ -0,0 +1,246 @@
|
||||
|
||||
/**
|
||||
* Client UI namespace.
|
||||
* @namespace
|
||||
*/
|
||||
GuacUI.Client = GuacUI.Client || {};
|
||||
|
||||
GuacUI.Client = {
|
||||
|
||||
/**
|
||||
* Collection of all Guacamole client UI states.
|
||||
*/
|
||||
"states": {
|
||||
|
||||
/**
|
||||
* The normal default Guacamole client UI mode
|
||||
*/
|
||||
"FULL_INTERACTIVE" : 0,
|
||||
|
||||
/**
|
||||
* Same as FULL_INTERACTIVE except with visible on-screen keyboard.
|
||||
*/
|
||||
"OSK" : 1,
|
||||
|
||||
/**
|
||||
* No on-screen keyboard, but a visible magnifier.
|
||||
*/
|
||||
"MAGNIFIER" : 2,
|
||||
|
||||
/**
|
||||
* Arrows and a draggable view.
|
||||
*/
|
||||
"PAN" : 3,
|
||||
|
||||
/**
|
||||
* Same as PAN, but with visible native OSK.
|
||||
*/
|
||||
"PAN_TYPING" : 4
|
||||
},
|
||||
|
||||
/* Constants */
|
||||
|
||||
"LONG_PRESS_DETECT_TIMEOUT" : 800, /* milliseconds */
|
||||
"LONG_PRESS_MOVEMENT_THRESHOLD" : 10, /* pixels */
|
||||
"KEYBOARD_AUTO_RESIZE_INTERVAL" : 30, /* milliseconds */
|
||||
|
||||
/* UI Components */
|
||||
|
||||
"viewport" : document.getElementById("viewportClone"),
|
||||
"display" : document.getElementById("display"),
|
||||
"logo" : document.getElementById("status-logo"),
|
||||
"eventTarget" : document.getElementById("eventTarget"),
|
||||
|
||||
"buttons": {
|
||||
"reconnect" : document.getElementById("reconnect")
|
||||
},
|
||||
|
||||
"containers": {
|
||||
"state" : document.getElementById("statusDialog"),
|
||||
"keyboard" : document.getElementById("keyboardContainer"),
|
||||
"magnifier" : document.getElementById("magnifier")
|
||||
},
|
||||
|
||||
"pan_overlay" : document.getElementById("pan-overlay"),
|
||||
"magnifier_background" : document.getElementById("magnifier-background"),
|
||||
"magnifier" : document.getElementById("magnifier-display"),
|
||||
"state" : document.getElementById("statusText"),
|
||||
"client" : null,
|
||||
"sessionState" : new GuacamoleSessionState()
|
||||
|
||||
};
|
||||
|
||||
GuacUI.Client.magnifier_background.onclick = function() {
|
||||
GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE);
|
||||
};
|
||||
|
||||
GuacUI.Client.containers.magnifier.addEventListener("click", function(e) {
|
||||
GuacUI.StateManager.setState(GuacUI.Client.states.PAN_TYPING);
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* Component which displays a magnified (100% zoomed) client display.
|
||||
*
|
||||
* @constructor
|
||||
* @augments GuacUI.DraggableComponent
|
||||
*/
|
||||
GuacUI.Client.Magnifier = function(magnifier) {
|
||||
|
||||
/**
|
||||
* Reference to this magnifier.
|
||||
* @private
|
||||
*/
|
||||
var guac_magnifier = this;
|
||||
|
||||
/*
|
||||
* Call parent constructor.
|
||||
*/
|
||||
GuacUI.DraggableComponent.apply(this, [magnifier]);
|
||||
|
||||
// Ensure transformations on display originate at 0,0
|
||||
GuacamoleUI.containers.magnifier.style.transformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.webkitTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.MozTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.OTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.msTransformOrigin =
|
||||
"0 0";
|
||||
|
||||
var magnifier_display = GuacUI.Client.magnifier;
|
||||
var magnifier_context = magnifier_display.getContext("2d");
|
||||
|
||||
this.onmove = function(x, y) {
|
||||
|
||||
var width = magnifier.offsetWidth;
|
||||
var height = magnifier.offsetHeight;
|
||||
|
||||
// Update contents relative to new position
|
||||
var clip_x = x
|
||||
/ (window.innerWidth - width) * (GuacamoleUI.client.getWidth() - width);
|
||||
var clip_y = y
|
||||
/ (window.innerHeight - height) * (GuacamoleUI.client.getHeight() - height);
|
||||
|
||||
magnifier_display.style.WebkitTransform =
|
||||
magnifier_display.style.MozTransform =
|
||||
magnifier_display.style.OTransform =
|
||||
magnifier_display.style.msTransform =
|
||||
magnifier_display.style.transform = "translate("
|
||||
+ (-clip_x) + "px, " + (-clip_y) + "px)";
|
||||
|
||||
GuacamoleUI.eventTarget.style.left = clip_x + "px";
|
||||
GuacamoleUI.eventTarget.style.top = clip_y + "px";
|
||||
|
||||
};
|
||||
|
||||
this.show = function() {
|
||||
|
||||
// Copy displayed image
|
||||
magnifier_display.width = GuacamoleUI.client.getWidth();
|
||||
magnifier_display.height = GuacamoleUI.client.getHeight();
|
||||
magnifier_context.drawImage(GuacamoleUI.client.flatten(), 0, 0);
|
||||
|
||||
// Show magnifier container
|
||||
GuacUI.Client.magnifier_background.style.display = "block";
|
||||
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
|
||||
// Hide magnifier container
|
||||
GuacUI.Client.magnifier_background.style.display = "none";
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* We inherit from GuacUI.DraggableComponent.
|
||||
*/
|
||||
GuacUI.Client.Magnifier.prototype = new GuacUI.DraggableComponent();
|
||||
|
||||
GuacUI.StateManager.registerComponent(
|
||||
new GuacUI.Client.Magnifier(GuacUI.Client.containers.magnifier),
|
||||
GuacUI.Client.states.MAGNIFIER
|
||||
);
|
||||
|
||||
/*
|
||||
* Zoomed Display
|
||||
*/
|
||||
|
||||
GuacUI.Client.ZoomedDisplay = function(client) {
|
||||
|
||||
var old_scale = null;
|
||||
|
||||
this.show = function() {
|
||||
old_scale = GuacamoleUI.client.getScale();
|
||||
GuacamoleUI.client.scale(1.0);
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
GuacamoleUI.client.scale(old_scale);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
GuacUI.Client.ZoomedDisplay.prototype = new GuacUI.Component();
|
||||
|
||||
GuacUI.StateManager.registerComponent(
|
||||
new GuacUI.Client.ZoomedDisplay(),
|
||||
GuacUI.Client.states.PAN,
|
||||
GuacUI.Client.states.PAN_TYPING
|
||||
);
|
||||
|
||||
/*
|
||||
* Pan UI
|
||||
*/
|
||||
|
||||
GuacUI.Client.PanOverlay = function(client) {
|
||||
|
||||
this.show = function() {
|
||||
GuacUI.Client.pan_overlay.style.display = "block";
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
GuacUI.Client.pan_overlay.style.display = "none";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
GuacUI.Client.PanOverlay.prototype = new GuacUI.Component();
|
||||
|
||||
GuacUI.StateManager.registerComponent(
|
||||
new GuacUI.Client.PanOverlay(),
|
||||
GuacUI.Client.states.PAN,
|
||||
GuacUI.Client.states.PAN_TYPING
|
||||
);
|
||||
|
||||
/*
|
||||
* Native Keyboard
|
||||
*/
|
||||
|
||||
GuacUI.Client.NativeKeyboard = function() {
|
||||
|
||||
this.show = function() {
|
||||
GuacamoleUI.eventTarget.focus();
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
GuacamoleUI.eventTarget.blur();
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
GuacUI.StateManager.registerComponent(
|
||||
new GuacUI.Client.NativeKeyboard(),
|
||||
GuacUI.Client.states.PAN_TYPING
|
||||
);
|
||||
|
||||
GuacUI.Client.eventTarget.onblur = function() {
|
||||
GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE);
|
||||
};
|
||||
|
||||
/*
|
||||
* Set initial state
|
||||
*/
|
||||
|
||||
GuacUI.StateManager.setState(GuacUI.Client.states.INTERACTIVE);
|
208
guacamole/src/main/webapp/scripts/guac-ui.js
Normal file
208
guacamole/src/main/webapp/scripts/guac-ui.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Guacamole - Clientless Remote Desktop
|
||||
* Copyright (C) 2010 Michael Jumper
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var GuacUI = GuacUI || {};
|
||||
|
||||
/**
|
||||
* Central registry of all components for all states.
|
||||
*/
|
||||
GuacUI.StateManager = new (function() {
|
||||
|
||||
/**
|
||||
* The current state.
|
||||
*/
|
||||
var current_state = null;
|
||||
|
||||
/**
|
||||
* Array of arrays of components, indexed by the states they are in.
|
||||
*/
|
||||
var components = [];
|
||||
|
||||
/**
|
||||
* Registers the given component with this state manager, to be shown
|
||||
* during the given states.
|
||||
*
|
||||
* @param {GuacUI.Component} component The component to register.
|
||||
* @param {Number} [...] The list of states this component should be
|
||||
* visible during.
|
||||
*/
|
||||
this.registerComponent = function(component) {
|
||||
|
||||
// For each state specified, add the given component
|
||||
for (var i=1; i<arguments.length; i++) {
|
||||
|
||||
// Get specified state
|
||||
var state = arguments[i];
|
||||
|
||||
// Get array of components in that state
|
||||
var component_array = components[state];
|
||||
if (!component_array)
|
||||
component_array = components[state] = [];
|
||||
|
||||
// Add component
|
||||
component_array.push(component);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function allComponents(components, name) {
|
||||
|
||||
// Invoke given function on all components in array
|
||||
for (var i=0; i<components.length; i++)
|
||||
components[i][name]();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current visible state.
|
||||
*/
|
||||
this.setState = function(state) {
|
||||
|
||||
// Hide components in current state
|
||||
if (current_state && components[current_state])
|
||||
allComponents(components[current_state], "hide");
|
||||
|
||||
// Show all components in new state
|
||||
current_state = state;
|
||||
if (components[state])
|
||||
allComponents(components[state], "show");
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* Abstract component which can be registered with GuacUI and shown or hidden
|
||||
* dynamically based on interface mode.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
GuacUI.Component = function() {
|
||||
|
||||
/**
|
||||
* Called whenever this component needs to be shown and activated.
|
||||
* @event
|
||||
*/
|
||||
this.onshow = null;
|
||||
|
||||
/**
|
||||
* Called whenever this component needs to be hidden and deactivated.
|
||||
* @event
|
||||
*/
|
||||
this.onhide = null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A Guacamole UI component which can be repositioned by dragging.
|
||||
*
|
||||
* @constructor
|
||||
* @augments GuacUI.Component
|
||||
*/
|
||||
GuacUI.DraggableComponent = function(element) {
|
||||
|
||||
var draggable_component = this;
|
||||
|
||||
var position_x = 0;
|
||||
var position_y = 0;
|
||||
|
||||
var start_x = 0;
|
||||
var start_y = 0;
|
||||
|
||||
/*
|
||||
* Record drag start when finger hits element
|
||||
*/
|
||||
if (element)
|
||||
element.addEventListener("touchstart", function(e) {
|
||||
|
||||
if (e.touches.length == 1) {
|
||||
|
||||
start_x = e.touches[0].screenX;
|
||||
start_y = e.touches[0].screenY;
|
||||
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
}, true);
|
||||
|
||||
/*
|
||||
* Update position based on last touch
|
||||
*/
|
||||
if (element)
|
||||
element.addEventListener("touchmove", function(e) {
|
||||
|
||||
if (e.touches.length == 1) {
|
||||
|
||||
var new_x = e.touches[0].screenX;
|
||||
var new_y = e.touches[0].screenY;
|
||||
|
||||
position_x += new_x - start_x;
|
||||
position_y += new_y - start_y;
|
||||
|
||||
start_x = new_x;
|
||||
start_y = new_y;
|
||||
|
||||
// Move magnifier to new position
|
||||
draggable_component.move(position_x, position_y);
|
||||
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
}, true);
|
||||
|
||||
if (element)
|
||||
element.addEventListener("touchend", function(e) {
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
|
||||
/**
|
||||
* Moves this component to the specified location relative to its normal
|
||||
* position.
|
||||
*
|
||||
* @param {Number} x The X coordinate in pixels.
|
||||
* @param {Number} y The Y coordinate in pixels.
|
||||
*/
|
||||
this.move = function(x, y) {
|
||||
|
||||
element.style.WebkitTransform =
|
||||
element.style.MozTransform =
|
||||
element.style.OTransform =
|
||||
element.style.msTransform =
|
||||
element.style.transform = "translate("
|
||||
+ x + "px, " + y + "px)";
|
||||
|
||||
if (draggable_component.onmove)
|
||||
draggable_component.onmove(x, y);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigered whenever this element is moved.
|
||||
*
|
||||
* @event
|
||||
* @param {Number} x The new X coordinate.
|
||||
* @param {Number} y The new Y coordinate.
|
||||
*/
|
||||
this.onmove = null;
|
||||
|
||||
};
|
@@ -114,124 +114,6 @@ GuacamoleUI.toggleKeyboard = function() {
|
||||
|
||||
};
|
||||
|
||||
GuacamoleUI.Magnifier = new (function() {
|
||||
|
||||
var guac_magnifier = this;
|
||||
|
||||
var position_x = 0;
|
||||
var position_y = 0;
|
||||
|
||||
var start_x = 0;
|
||||
var start_y = 0;
|
||||
|
||||
GuacamoleUI.containers.magnifier.addEventListener("touchstart", function(e) {
|
||||
|
||||
if (e.touches.length == 1) {
|
||||
|
||||
start_x = e.touches[0].screenX;
|
||||
start_y = e.touches[0].screenY;
|
||||
|
||||
}
|
||||
|
||||
}, false);
|
||||
|
||||
GuacamoleUI.containers.magnifier.addEventListener("touchmove", function(e) {
|
||||
|
||||
if (e.touches.length == 1) {
|
||||
|
||||
var width = GuacamoleUI.containers.magnifier.offsetWidth;
|
||||
var height = GuacamoleUI.containers.magnifier.offsetHeight;
|
||||
|
||||
var new_x = e.touches[0].screenX;
|
||||
var new_y = e.touches[0].screenY;
|
||||
|
||||
position_x += new_x - start_x;
|
||||
position_y += new_y - start_y;
|
||||
|
||||
if (position_x < 0) position_x = 0;
|
||||
else if (position_x > window.innerWidth - width)
|
||||
position_x = window.innerWidth - width;
|
||||
|
||||
if (position_y < 0) position_y = 0;
|
||||
else if (position_y > window.innerHeight - height)
|
||||
position_y = window.innerHeight - height;
|
||||
|
||||
start_x = new_x;
|
||||
start_y = new_y;
|
||||
|
||||
// Move magnifier to new position
|
||||
guac_magnifier.move(position_x, position_y);
|
||||
|
||||
// Update contents relative to new position
|
||||
var clip_x = position_x
|
||||
/ (window.innerWidth - width) * (GuacamoleUI.client.getWidth() - width);
|
||||
var clip_y = position_y
|
||||
/ (window.innerHeight - height) * (GuacamoleUI.client.getHeight() - height);
|
||||
|
||||
GuacamoleUI.magnifier.style.WebkitTransform =
|
||||
GuacamoleUI.magnifier.style.MozTransform =
|
||||
GuacamoleUI.magnifier.style.OTransform =
|
||||
GuacamoleUI.magnifier.style.msTransform =
|
||||
GuacamoleUI.magnifier.style.transform = "translate("
|
||||
+ (-clip_x) + "px, " + (-clip_y) + "px)";
|
||||
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
GuacamoleUI.containers.magnifier.addEventListener("click", function(e) {
|
||||
GuacamoleUI.eventTarget.focus();
|
||||
}, false);
|
||||
|
||||
this.move = function(x, y) {
|
||||
|
||||
GuacamoleUI.containers.magnifier.style.WebkitTransform =
|
||||
GuacamoleUI.containers.magnifier.style.MozTransform =
|
||||
GuacamoleUI.containers.magnifier.style.OTransform =
|
||||
GuacamoleUI.containers.magnifier.style.msTransform =
|
||||
GuacamoleUI.containers.magnifier.style.transform = "translate("
|
||||
+ x + "px, " + y + "px)";
|
||||
|
||||
};
|
||||
|
||||
this.update = function() {
|
||||
|
||||
GuacamoleUI.magnifierContext.drawImage(GuacamoleUI.client.flatten(),
|
||||
0, 0);
|
||||
|
||||
};
|
||||
|
||||
this.show = function(x, y) {
|
||||
|
||||
// Get client dimensions
|
||||
var width = GuacamoleUI.client.getWidth();
|
||||
var height = GuacamoleUI.client.getHeight();
|
||||
|
||||
// Resize to fit client
|
||||
GuacamoleUI.magnifier.width = width;
|
||||
GuacamoleUI.magnifier.height = height;
|
||||
|
||||
// Ensure transformations on display originate at 0,0
|
||||
GuacamoleUI.containers.magnifier.style.transformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.webkitTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.MozTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.OTransformOrigin =
|
||||
GuacamoleUI.containers.magnifier.style.msTransformOrigin =
|
||||
"0 0";
|
||||
|
||||
// Show magnifier
|
||||
GuacamoleUI.containers.magnifier.style.display = "block";
|
||||
guac_magnifier.update();
|
||||
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
GuacamoleUI.containers.magnifier.style.display = "none";
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
// If Node.classList is supported, implement addClass/removeClass using that
|
||||
if (Node.classList) {
|
||||
|
||||
@@ -678,7 +560,7 @@ GuacamoleUI.attach = function(guac) {
|
||||
|
||||
longPressTimeout = window.setTimeout(function() {
|
||||
longPressTimeout = null;
|
||||
GuacamoleUI.Magnifier.show();
|
||||
GuacUI.StateManager.setState(GuacUI.Client.states.MAGNIFIER);
|
||||
}, GuacamoleUI.LONG_PRESS_DETECT_TIMEOUT);
|
||||
|
||||
}
|
||||
@@ -692,9 +574,6 @@ GuacamoleUI.attach = function(guac) {
|
||||
// Detect long-press at bottom of screen
|
||||
GuacamoleUI.display.addEventListener('touchstart', function(e) {
|
||||
|
||||
// Close magnifier
|
||||
GuacamoleUI.Magnifier.hide();
|
||||
|
||||
// Record touch location
|
||||
if (e.touches.length == 1) {
|
||||
var touch = e.touches[0];
|
||||
|
@@ -42,9 +42,11 @@ img {
|
||||
}
|
||||
|
||||
#eventTarget {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -170,13 +172,22 @@ div#display > * {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
div#magnifier {
|
||||
|
||||
div#magnifier-background {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div#magnifier {
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.75);
|
||||
width: 50%;
|
||||
@@ -185,6 +196,15 @@ div#magnifier {
|
||||
|
||||
}
|
||||
|
||||
div#pan-overlay {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Viewport Clone */
|
||||
|
||||
div#viewportClone {
|
||||
|
Reference in New Issue
Block a user