Skip to content

Commit d8428c1

Browse files
author
Eric Koleda
committed
Added Cursor Inspector sample.
1 parent 217cf5c commit d8428c1

File tree

5 files changed

+381
-0
lines changed

5 files changed

+381
-0
lines changed

cursor_inspector/Code.gs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Runs when the document is opened.
3+
*/
4+
function onOpen() {
5+
DocumentApp.getUi().createMenu('Inspector')
6+
.addItem('Show sidebar', 'showSidebar')
7+
.addToUi();
8+
}
9+
10+
/**
11+
* Show the sidebar.
12+
*/
13+
function showSidebar() {
14+
DocumentApp.getUi().showSidebar(
15+
HtmlService.createTemplateFromFile('Sidebar').evaluate()
16+
.setSandboxMode(HtmlService.SandboxMode.NATIVE)
17+
.setTitle('Cursor Inspector')
18+
.setWidth(350));
19+
}
20+
21+
/**
22+
* Returns the contents of an HTML file.
23+
* @param {string} file The name of the file to retrieve.
24+
* @return {string} The content of the file.
25+
*/
26+
function include(file) {
27+
return HtmlService.createTemplateFromFile(file).evaluate().getContent();
28+
}
29+
30+
/**
31+
* Gets the current cursor and selector information for the document.
32+
* @return {Object} The infomration.
33+
*/
34+
function getDocumentInfo() {
35+
var document = DocumentApp.getActiveDocument();
36+
var cursor = document.getCursor();
37+
var selection = document.getSelection();
38+
var result = {};
39+
if (cursor) {
40+
result.cursor = {
41+
element: getElementInfo(cursor.getElement()),
42+
offset: cursor.getOffset(),
43+
surroundingText: cursor.getSurroundingText().getText(),
44+
surroundingTextOffset: cursor.getSurroundingTextOffset()
45+
};
46+
}
47+
if (selection) {
48+
result.selection = {
49+
selectedElements: selection.getSelectedElements().map(function(selectedElement) {
50+
return {
51+
element: getElementInfo(selectedElement.getElement()),
52+
partial: selectedElement.isPartial(),
53+
startOffset: selectedElement.getStartOffset(),
54+
endOffsetInclusive: selectedElement.getEndOffsetInclusive()
55+
}
56+
})
57+
}
58+
}
59+
return result;
60+
}
61+
62+
/**
63+
* Gets information about a given element.
64+
* @param {Element} element The element.
65+
* @return {Object} The information.
66+
*/
67+
function getElementInfo(element) {
68+
return {
69+
type: String(element.getType()),
70+
};
71+
}

cursor_inspector/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Cursor Inspector
2+
3+
Cursor Inspector is a sample script for Google Docs that allows you to inspect
4+
the current state of the cursor or selection within a document. The information
5+
is presented in a sidebar and updates automatically every few seconds. The data
6+
presented corresponds with the
7+
[`Cursor`](https://developers.google.com/apps-script/reference/document/cursor)
8+
and
9+
[`Selection`](https://developers.google.com/apps-script/reference/document/selection)
10+
classes of the API.
11+
12+
## Try it out
13+
14+
For your convience we have deployed the script into a Google Docs
15+
[document](https://docs.google.com/document/d/1v6S7IkDL_YIaVn1rBcVbqFr3rbNUX9_kLfFc00WTtx8/edit)
16+
that you can copy and use. Follow the instructions in the document to get
17+
started.

cursor_inspector/Sidebar.css.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!-- External Libraries -->
2+
<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.0/themes/smoothness/jquery-ui.css" />
3+
4+
<!-- Custom Styles -->
5+
<style>
6+
html {
7+
height: 100%;
8+
}
9+
body {
10+
font-family: arial, sans-serif;
11+
font-size: 13px;
12+
height: 100%;
13+
padding: 10px 12px;
14+
}
15+
table {
16+
border-spacing: 0;
17+
}
18+
td {
19+
vertical-align: top;
20+
}
21+
fieldset {
22+
margin-bottom: 1em;
23+
}
24+
input {
25+
width: 160px;
26+
}
27+
28+
.na {
29+
color: gray;
30+
text-align: center;
31+
}
32+
33+
#results {
34+
height: 99%;
35+
overflow: auto;
36+
}
37+
#selection table {
38+
width: 100%;
39+
}
40+
#selection th, #selection td {
41+
border: 1px solid gray;
42+
}
43+
#error {
44+
background-color: red;
45+
color: white;
46+
display: none;
47+
padding: 1em;
48+
}
49+
#loading {
50+
position: absolute;
51+
top: 0;
52+
left: 0;
53+
width: 100%;
54+
height: 100%;
55+
/* background-color: rgba(200,200,200,0.6); */
56+
background-color: rgba(200,200,200,1);
57+
font-weight: bold;
58+
}
59+
#loading-content {
60+
position: relative;
61+
top: 50%;
62+
margin: 0 auto;
63+
width: 100px;
64+
height: 50px;
65+
line-height: 50px;
66+
vertical-align: middle;
67+
background-color: white;
68+
border: 1px solid black;
69+
text-align: center;
70+
}
71+
</style>

cursor_inspector/Sidebar.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?!= include('Sidebar.css') ?>
2+
3+
<div id="loading">
4+
<div id="loading-content">Loading ...</div>
5+
</div>
6+
7+
<div id="error"></div>
8+
9+
<div id="results">
10+
<fieldset id="cursor">
11+
<legend>Cursor</legend>
12+
<table>
13+
<tr>
14+
<td><label>Element Type:</label></td>
15+
<td><input id="element-type" type="text" /></td>
16+
</tr>
17+
<tr>
18+
<td><label>Offset:</label></td>
19+
<td><input id="offset" type="text" /></td>
20+
</tr>
21+
<tr>
22+
<td><label>Surrounding Text:</label></td>
23+
<td><input id="surrounding-text" type="text" /></td>
24+
</tr>
25+
<tr>
26+
<td><label>Surrounding Text Offset:</label></td>
27+
<td><input id="surrounding-text-offset" type="text" /></td>
28+
</tr>
29+
</table>
30+
</fieldset>
31+
32+
<fieldset id="selection">
33+
<legend>Selection</legend>
34+
<table>
35+
<tr>
36+
<th>Element</th>
37+
<th>Partial</th>
38+
<th>Start</th>
39+
<th>End</th>
40+
</tr>
41+
<tbody></tbody>
42+
</table>
43+
</fieldset>
44+
45+
<p>
46+
Automatically refreshed every few seconds.<br/>
47+
Last updated <strong id="lastupdated"></strong>.
48+
</p>
49+
<button id="toggle" onclick="toggleRefresh();" data-stoptext="◾ Stop" data-resumetext="▶ Resume">◾ Stop</button>
50+
</div>
51+
52+
<?!= include('Sidebar.js') ?>

cursor_inspector/Sidebar.js.html

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!-- Libraries -->
2+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
3+
<script src="http://code.jquery.com/ui/1.10.0/jquery-ui.min.js"></script>
4+
5+
<!-- Custom JavaScript -->
6+
<script>
7+
// Constants.
8+
var REFRESH_WAIT_SECONDS = 1;
9+
var INACTIVITY_TIMEOUT_MINUTES = 2;
10+
11+
// Global variables.
12+
var lastResult;
13+
var refreshOn = true;
14+
var lastChangedTime;
15+
var timeoutId;
16+
17+
// On page load.
18+
$(function() {
19+
$('#loading').hide();
20+
refreshState();
21+
});
22+
23+
/**
24+
* Refreshes the state information in the sidebar.
25+
*/
26+
function refreshState() {
27+
google.script.run.withFailureHandler(function(error) {
28+
showError(error);
29+
tick(null);
30+
}).withSuccessHandler(function(result) {
31+
hideError();
32+
if (!lastResult || !jsonEquals(result.cursor, lastResult.cursor)) {
33+
updateCursor(result.cursor);
34+
}
35+
if (!lastResult || !jsonEquals(result.selection, lastResult.selection)) {
36+
updateSelection(result.selection);
37+
}
38+
tick(result);
39+
}).getDocumentInfo();
40+
}
41+
42+
/**
43+
* Updates the cursor information in the sidebar.
44+
* @param {Object} cursor The cursor information.
45+
*/
46+
function updateCursor(cursor) {
47+
if (cursor) {
48+
$('#cursor input').removeAttr('disabled');
49+
updateElement('element-type', cursor.element.type);
50+
updateElement('offset', cursor.offset);
51+
updateElement('surrounding-text', cursor.surroundingText);
52+
updateElement('surrounding-text-offset', cursor.surroundingTextOffset);
53+
} else {
54+
$('#cursor input').val('').attr('disabled', 'true');
55+
}
56+
}
57+
58+
/**
59+
* Updates the selection information in the sidebar.
60+
* @param {Object} selection The selection information.
61+
*/
62+
function updateSelection(selection) {
63+
var tableBody = $('#selection table tbody');
64+
tableBody.children().remove();
65+
if (selection) {
66+
selection.selectedElements.forEach(function(selectedElement) {
67+
var row = $('<tr>');
68+
var data = [
69+
selectedElement.element.type,
70+
selectedElement.partial,
71+
selectedElement.startOffset,
72+
selectedElement.endOffsetInclusive
73+
];
74+
data.forEach(function(value) {
75+
row.append($('<td>').text(value));
76+
});
77+
tableBody.append(row);
78+
});
79+
tableBody.effect("highlight", { duration: 1500 });
80+
} else {
81+
tableBody.append($('<tr><td class="na" colspan="4">None</td></tr>'));
82+
}
83+
}
84+
85+
/**
86+
* Shows an error message in the sidebar.
87+
* @param {string} error The error returned by the server.
88+
*/
89+
function showError(error) {
90+
$('#error').text(error).show();
91+
$('#results').css('color', 'gray');
92+
}
93+
94+
/**
95+
* Hides any error message in the sidebar.
96+
*/
97+
function hideError() {
98+
$('#error').hide();
99+
$('#results').css('color', 'inherit');
100+
}
101+
102+
/**
103+
* Updates the state of the document and sets up the next refresh.
104+
* @param {Object} result The last result, if any.
105+
*/
106+
function tick(result) {
107+
if (result) {
108+
if (!jsonEquals(result, lastResult)) {
109+
lastChangedTime = new Date();
110+
}
111+
lastResult = result;
112+
}
113+
$('#lastupdated').text(new Date().toLocaleTimeString());
114+
if (isInactive()) {
115+
toggleRefresh();
116+
lastResult = null;
117+
lastChangedTime = null;
118+
}
119+
if (refreshOn) {
120+
timeoutId = window.setTimeout(refreshState, REFRESH_WAIT_SECONDS * 1000);
121+
}
122+
}
123+
124+
/**
125+
* Determines if the document is inactive.
126+
* @return {Boolean} True if the document is inactive, false otherwise.
127+
*/
128+
function isInactive() {
129+
var now = new Date();
130+
return lastChangedTime && now.getTime() - lastChangedTime.getTime() > INACTIVITY_TIMEOUT_MINUTES * 60 * 1000;
131+
}
132+
133+
/**
134+
* Toggles whether or not automatic refreshing is on, and updated the button.
135+
*/
136+
function toggleRefresh() {
137+
var toggle = $('#toggle');
138+
if (refreshOn) {
139+
refreshOn = false;
140+
window.clearTimeout(timeoutId);
141+
toggle.text(toggle.data('resumetext'));
142+
} else {
143+
refreshOn = true;
144+
refreshState();
145+
toggle.text(toggle.data('stoptext'));
146+
}
147+
}
148+
149+
/**
150+
* Updates an element with a new value and highlights it if there is a change.
151+
* @param {string} elementId The ID of the element to update.
152+
* @param {string} value The new value of the element.
153+
*/
154+
function updateElement(elementId, value) {
155+
var element = $(document.getElementById(elementId));
156+
if (String(element.val()) != String(value)) {
157+
element.val(value).effect("highlight", { duration: 1500 });
158+
}
159+
}
160+
161+
/**
162+
* Determines if two Objects have the JSON structure.
163+
* @param {Object} a The first object.
164+
* @param {Object} b The second object.
165+
* @return {Boolean} True if both objects have the same JSON structure, false otherwise.
166+
*/
167+
function jsonEquals(a, b) {
168+
return JSON.stringify(a) == JSON.stringify(b);
169+
}
170+
</script>

0 commit comments

Comments
 (0)