PR 12: Merge feature/agave to master

- Add calendar XML file
 - Restyling - prep for reload of calendar
 - Link up calendar reloading
 - Consolidate loading code
 - Fix proxy objects for office.js case to work with reloads
 - fix IE11 button focus styling
This commit is contained in:
Kurt Berglund 2016-10-15 19:25:11 +00:00
Родитель 674ae7f10c 178f3f8719
Коммит 4d72aecb83
8 изменённых файлов: 357 добавлений и 164 удалений

32
server/Calendar-local.xml Normal file
Просмотреть файл

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ContentApp">
<Id>A1EB8AB4-5807-4D99-9A70-26F65D933ED6</Id>
<Version>1.12.0.0</Version>
<ProviderName>Microsoft</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Calendar (local)">
</DisplayName>
<Description DefaultValue="Calendar control">
</Description>
<IconUrl DefaultValue="https://edera.cloudapp.net/images/ChartTypeArea.32x32x32.png">
</IconUrl>
<HighResolutionIconUrl DefaultValue="https://edera.cloudapp.net/images/ChartTypeArea.64x64x32.png">
</HighResolutionIconUrl>
<SupportUrl DefaultValue="http://aka.ms/mixhelp"></SupportUrl>
<Hosts>
<Host Name="Workbook"/>
</Hosts>
<AppDomains>
<AppDomain>https://login.microsoftonline.com</AppDomain>
<AppDomain>https://login.live.com</AppDomain>
<AppDomain>https://accounts.google.com</AppDomain>
</AppDomains>
<DefaultSettings>
<SourceLocation DefaultValue="http://localhost:3000/loader"></SourceLocation>
<RequestedWidth>640</RequestedWidth>
<RequestedHeight>480</RequestedHeight>
</DefaultSettings>
<Permissions>ReadWriteDocument</Permissions>
<!--Allowed snapshot can be true or false for InContent Agaves only-->
<AllowSnapshot>true</AllowSnapshot>
</OfficeApp>

32
server/Calendar.xml Normal file
Просмотреть файл

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ContentApp">
<Id>5CBA2DDA-F8EC-4C6D-A070-11E24E123226</Id>
<Version>1.12.0.0</Version>
<ProviderName>Microsoft</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Calendar">
</DisplayName>
<Description DefaultValue="Calendar control">
</Description>
<IconUrl DefaultValue="https://edera.cloudapp.net/images/ChartTypeArea.32x32x32.png">
</IconUrl>
<HighResolutionIconUrl DefaultValue="https://edera.cloudapp.net/images/ChartTypeArea.64x64x32.png">
</HighResolutionIconUrl>
<SupportUrl DefaultValue="http://aka.ms/mixhelp"></SupportUrl>
<Hosts>
<Host Name="Workbook"/>
</Hosts>
<AppDomains>
<AppDomain>https://login.microsoftonline.com</AppDomain>
<AppDomain>https://login.live.com</AppDomain>
<AppDomain>https://accounts.google.com</AppDomain>
</AppDomains>
<DefaultSettings>
<SourceLocation DefaultValue="https://officenet.azurewebsites.net/loader"></SourceLocation>
<RequestedWidth>640</RequestedWidth>
<RequestedHeight>480</RequestedHeight>
</DefaultSettings>
<Permissions>ReadWriteDocument</Permissions>
<!--Allowed snapshot can be true or false for InContent Agaves only-->
<AllowSnapshot>true</AllowSnapshot>
</OfficeApp>

Просмотреть файл

@ -1 +1 @@
html{width:100%;height:100%}body{margin:0px;padding:0px;overflow:hidden;width:100%;height:100%}
html{width:100%;height:100%;overflow:hidden}body{margin:0px;padding:0px;overflow:hidden;width:100%;height:100%}

Просмотреть файл

@ -1,6 +1,7 @@
html {
width: 100%;
height: 100%;
overflow: hidden;
}
body {

Просмотреть файл

@ -1,26 +1,16 @@
import * as $ from 'jquery';
import * as _ from 'lodash';
import { pnhost, IEchoService, EchoServiceName, ITableService, TableServiceName, ITableListener } from '../api/index';
import { Promise } from 'es6-promise';
import { pnhost, IEchoService, EchoServiceName, ITable, ITableService, TableServiceName, ITableListener } from '../api/index';
var fullcalendar = require('fullcalendar');
var qtip = require('qtip2');
// TODO better wrap this into some kind of model structure
var rowsModel: any[] = [];
var pendingDeletes: any[] = [];
class TableListener implements ITableListener {
constructor(private _viewModel: CalendarViewModel) {
}
class TableListener implements ITableListener {
rowsChanged(rows: any[]): Promise<void> {
// compute the difference between the existing rowsModel and the updates
let deletedRows = _.filter(rowsModel, (rowModel) => _.find(rows, (row) => row.id === rowModel.id) === undefined);
$('#calendar').fullCalendar('removeEvents', (event) => {
return _.find(deletedRows, (deletedRow) => deletedRow.id === event.id);
});
// Add the rows to delete to the pending list
pendingDeletes = pendingDeletes.concat(deletedRows);
// Update the model with the changed rows
rowsModel = rows;
this._viewModel.rowsChanged(rows);
return Promise.resolve();
}
@ -30,113 +20,208 @@ class TableListener implements ITableListener {
}
}
$(document).ready(() => {
let tableServiceP: Promise<ITableService>;
if (pnhost) {
tableServiceP = pnhost.listServices().then((services) => {
return _.includes(services, TableServiceName) ? pnhost.getService(TableServiceName) : Promise.resolve(null);
class RemoteCalendar {
constructor() {
}
getCalendars(): Promise<Calendar[]> {
return new Promise((resolve, reject) => {
$.ajax("/calendars", {
cache: false,
dataType: "json",
success: (calData: Calendar[]) => resolve(calData),
error: (jqXHR: JQueryXHR, textStatus: string, errorThrown: string) => reject(jqXHR)
});
});
}
else {
tableServiceP = Promise.resolve(null);
}
class CalendarViewModel {
private _calendar = new RemoteCalendar();
private _tableServiceP: Promise<ITableService>;
private _cachedCalendars: Calendar[];
private _pendingDeletes: any[] = [];
private _tableP: Promise<ITable>;
private _tableListener: TableListener;
private _tableBoundRows: any[];
constructor() {
}
$.ajax("/calendars", {
cache: false,
dataType: "json",
success: (calData: Calendar[]) => {
if (pnhost) {
$("#buttons").append($('<button id="load-table">Export to Table</button><button id="save">Save</button>'));
// Save processes any pending calendar changes
$("#save").click(() => {
// iterate over the deletedRows URLs and delete them
for (let pendingDelete of pendingDeletes) {
$.ajax(pendingDelete.self, { method: "DELETE" });
console.log(`DELETE: ${pendingDelete.self}`);
}
});
private initView() {
$("#buttons").append($('<div class="fc-right"><div class="fc-button-group"><button id="reload" class="fc-button fc-state-default" type="button">Reload</button></div></div>'));
$("#reload").click(() => {
this.loadAndCacheCalendars();
});
$("#load-table").click(() => {
tableServiceP.then((tableService) => {
if (!tableService) {
return;
}
if (pnhost) {
$("#buttons").append($('<div class="fc-left"><div class="fc-button-group"><button id="load-table" class="fc-button fc-state-default" type="button">Export to Table</button></div></div>'));
$("#buttons .fc-right .fc-button-group").append('<button id="save" class="fc-button fc-state-default" type="button">Save</button>');
tableService.createTable().then((table) => {
let columns = ['provider', 'title', 'location', 'start', 'end', 'responseStatus'];
for (let calendar of calData) {
for (let event of calendar.events) {
rowsModel.push({
id: event.id,
provider: calendar.sourceName,
title: event.title,
location: event.location,
start: event.start,
end: event.end,
responseStatus: event.responseStatus,
self: event.self
})
}
}
table.loadData(columns, rowsModel);
// Listen for updates
table.addListener(new TableListener());
});
})
});
}
// page is now ready, initialize the calendar...
var fcOptions = <FullCalendar.Options>{
minTime: "07:00:00",
maxTime: "21:00:00",
weekends: false,
height: "auto",
eventRender: (event, element) => {
var content = event.title;
if (event.location && (event.location.length > 0)) {
content += ("<br/>" + event.location);
}
if (event.responseStatus && (event.responseStatus.length > 0)) {
content += ("<br/>" + event.responseStatus);
}
var qtipOptions: QTip2.QTipOptions = {
content: content,
position: {
my: "left center",
at: "center"
}
};
element.qtip(qtipOptions);
// Save processes any pending calendar changes
$("#save").click(() => {
// iterate over the deletedRows URLs and delete them
for (let pendingDelete of this._pendingDeletes) {
$.ajax(pendingDelete.self, { method: "DELETE" });
console.log(`DELETE: ${pendingDelete.self}`);
}
};
});
var events: FullCalendar.EventObject[] = [];
for (var ncal = calData.length, ical = 0; ical < ncal; ical++) {
var cal = calData[ical];
var borderColor = (ical == 0) ? "black" : "darkblue";
var color = (ical == 0) ? "purple" : "green";
$("#load-table").click(() => {
// Disable future loads since we're now bound to the host
$("#load-table").prop('disabled', true);
for (var nevent = cal.events.length, iev = 0; iev < nevent; iev++) {
let ev = cal.events[iev];
events.push({
id: ev.id,
title: ev.title,
color: color,
borderColor: borderColor,
location: ev.location,
responseStatus: ev.responseStatus,
start: new Date(ev.start),
end: new Date(ev.end),
});
this.loadPNHostTable();
});
}
var fcOptions = <FullCalendar.Options>{
minTime: "07:00:00",
maxTime: "21:00:00",
weekends: false,
height: "auto",
eventRender: (event, element) => {
var content = event.title;
if (event.location && (event.location.length > 0)) {
content += ("<br/>" + event.location);
}
if (event.responseStatus && (event.responseStatus.length > 0)) {
content += ("<br/>" + event.responseStatus);
}
var qtipOptions: QTip2.QTipOptions = {
content: content,
position: {
my: "left center",
at: "center"
}
};
element.qtip(qtipOptions);
}
fcOptions.events = events;
$('#calendar').fullCalendar(fcOptions);
$('#calendar').fullCalendar('changeView', 'agendaWeek');
};
$('#calendar').fullCalendar(fcOptions);
$('#calendar').fullCalendar('changeView', 'agendaWeek');
}
private loadPNHostTable() {
if (!this._tableP) {
this._tableP = this._tableServiceP.then((tableService) => tableService.createTable());
}
});
});
this._tableP.then((table) => {
let columns = ['provider', 'title', 'location', 'start', 'end', 'responseStatus'];
// Get and store the rows we will bind to the pnhost table
this._tableBoundRows = [];
for (let calendar of this._cachedCalendars) {
for (let event of calendar.events) {
this._tableBoundRows.push({
id: event.id,
provider: calendar.sourceName,
title: event.title,
location: event.location,
start: event.start,
end: event.end,
responseStatus: event.responseStatus,
self: event.self
})
}
}
// Load the rows into the hosted table
table.loadData(columns, this._tableBoundRows);
// Setup a table listener if it doesn't already exist
if (!this._tableListener) {
this._tableListener = new TableListener(this);
table.addListener(this._tableListener);
}
})
}
private loadAndCacheCalendars(): Promise<Calendar[]> {
return this._calendar.getCalendars().then((calendars) => {
// Clear any pending deletes - a reload resets any interactions
this._pendingDeletes = [];
// Initialize the custom UI once we load the first batch of data
if (this._cachedCalendars === undefined) {
this.initView();
}
this._cachedCalendars = calendars;
// Update the calendar UI
this.loadCalendarView(calendars);
// Refresh the pnhost table with the new fields
if (this._tableP) {
this.loadPNHostTable();
}
return calendars;
})
}
rowsChanged(rows: any[]) {
// compute the difference between the received rows and the bound data
let deletedRows = _.filter(this._tableBoundRows, (rowModel) => _.find(rows, (row) => row.id === rowModel.id) === undefined);
$('#calendar').fullCalendar('removeEvents', (event) => {
return _.find(deletedRows, (deletedRow) => deletedRow.id === event.id);
});
// Add the rows to delete to the pending list
this._pendingDeletes = this._pendingDeletes.concat(deletedRows);
// Update the bound data values
this._tableBoundRows = rows;
}
private loadCalendarView(calendars: Calendar[]) {
var events: FullCalendar.EventObject[] = [];
for (var ncal = calendars.length, ical = 0; ical < ncal; ical++) {
var cal = calendars[ical];
var borderColor = (ical == 0) ? "black" : "darkblue";
var color = (ical == 0) ? "purple" : "green";
for (var nevent = cal.events.length, iev = 0; iev < nevent; iev++) {
let ev = cal.events[iev];
events.push({
id: ev.id,
title: ev.title,
color: color,
borderColor: borderColor,
location: ev.location,
responseStatus: ev.responseStatus,
start: new Date(ev.start),
end: new Date(ev.end),
});
}
}
// Update the calendar view
$('#calendar').fullCalendar('removeEvents');
for (let event of events) {
$('#calendar').fullCalendar('renderEvent', event);
}
}
init() {
if (pnhost) {
this._tableServiceP = pnhost.listServices().then((services) => {
return _.includes(services, TableServiceName) ? pnhost.getService(TableServiceName) : Promise.resolve(null);
});
}
else {
this._tableServiceP = Promise.resolve(null);
}
$(document).ready(() => {
this.loadAndCacheCalendars();
});
}
}
// Create and initialize the view model that will bind to and run the UI
let viewModel = new CalendarViewModel();
viewModel.init();

Просмотреть файл

@ -27,16 +27,21 @@ class Table implements ITable {
loadData(columns: string[], rows: any[]): Promise<void> {
console.log('load data');
let columnOptions = columns.map((column) => new TableColumn({prop: column}));
// TODO There's a bug in the angular table where changing column options adds extra padding
// to the table. So assuming the columns don't change between loads for now.
if (!this.options) {
this.options = new TableOptions({
columnMode: ColumnMode.force,
headerHeight: 50,
footerHeight: 50,
rowHeight: 'auto',
selectionType: SelectionType.multi,
columns: columnOptions
});
}
this.options = new TableOptions({
columnMode: ColumnMode.force,
headerHeight: 50,
footerHeight: 50,
rowHeight: 'auto',
selectionType: SelectionType.multi,
columns: columnOptions
});
this.selection = [];
this.rows = rows;
return Promise.resolve();

Просмотреть файл

@ -1,9 +1,26 @@
<head>
<link rel='stylesheet' href='/node_modules/fullcalendar/dist/fullcalendar.css' />
<link rel='stylesheet' href='/node_modules/qtip2/dist/jquery.qtip.css' />
<script src='/dist/views/calendar/driver.js'></script>
<script src='/dist/views/calendar/driver.js'></script>
<style>
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.mui-controls {
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id='buttons'></div>
<div class="fc clearfix mui-controls">
<div id='buttons' class="fc-toolbar">
</div>
</div>
<div id='calendar'></div>
</body>

Просмотреть файл

@ -3,7 +3,7 @@
<link href="/stylesheets/embed.css" rel="stylesheet" />
{{/styles}}
{{$content}}
{{$content}}
<iframe src="/documents/calendar" frameborder="0" style="overflow:hidden;height:100%;width:100%" height="100%" width="100%"></iframe>
{{/content}}
@ -84,44 +84,65 @@
// Run a batch operation against the Excel object model
Excel.run(function (ctx) {
// Create a proxy object for the active worksheet
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
if (that.table) {
var tableRows = ctx.workbook.tables.getItem('calendarTable').rows;
tableRows.load('items');
return ctx.sync().then(function() {
for (var i = tableRows.items.length - 1; i >= 0; i--) {
tableRows.items[i].delete();
}
// I imagine I can do C style 'a' + number - but not sure how yet in JS
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var endPosition = possible.charAt(columns.length - 1) + 1;
var table = sheet.tables.add('A1:' + endPosition, true);
table.name = "calendarTable";
table.getHeaderRowRange().values = [columns];
var rowsToLoad = [];
for (var i = 0; i < rows.length; i++) {
var newRow = [];
for (var j = 0; j < columns.length; j++) {
newRow.push(rows[i][columns[j]]);
}
table.rows.add(null, [newRow]);
}
//Run the queued commands, and return a promise to indicate task completion
return ctx.sync().then(function() {
//Create a new table binding for myTable
Office.context.document.bindings.addFromNamedItemAsync(
"calendarTable",
Office.CoercionType.Table,
{ id: "myBinding" },
function (asyncResult) {
if (asyncResult.status == "failed") {
console.log("Action failed with error: " + asyncResult.error.message);
}
else {
// If successful, add the event handler to the table binding.
Office.select("bindings#myBinding").addHandlerAsync(Office.EventType.BindingDataChanged, function() {
that.displayDataForBinding();
});
}
});
});
var rowsToLoad = [];
for (var i = 0; i < rows.length; i++) {
var newRow = [];
for (var j = 0; j < columns.length; j++) {
newRow.push(rows[i][columns[j]]);
}
tableRows.add(null, [newRow]);
}
});
}
else {
// Create a proxy object for the active worksheet
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
// I imagine I can do C style 'a' + number - but not sure how yet in JS
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
var endPosition = possible.charAt(columns.length - 1) + 1;
var table = sheet.tables.add('A1:' + endPosition, true);
that.table = table;
table.name = "calendarTable";
table.getHeaderRowRange().values = [columns];
var rowsToLoad = [];
for (var i = 0; i < rows.length; i++) {
var newRow = [];
for (var j = 0; j < columns.length; j++) {
newRow.push(rows[i][columns[j]]);
}
table.rows.add(null, [newRow]);
}
//Run the queued commands, and return a promise to indicate task completion
return ctx.sync().then(function() {
//Create a new table binding for myTable
Office.context.document.bindings.addFromNamedItemAsync(
"calendarTable",
Office.CoercionType.Table,
{ id: "myBinding" },
function (asyncResult) {
if (asyncResult.status == "failed") {
console.log("Action failed with error: " + asyncResult.error.message);
}
else {
// If successful, add the event handler to the table binding.
Office.select("bindings#myBinding").addHandlerAsync(Office.EventType.BindingDataChanged, function() {
that.displayDataForBinding();
});
}
});
});
}
})
.then(function () {
console.log("Success!");