Exporting case details from FogBugz - fogbugz

We are seeking to create a snapshot of all our FogBugz data however the GUI only seems to allow to export the top level list of cases, not their detail/attachments. Searching around the only solutions I can find relate to apps that are now out of date.

I found this very hard to find any help on but eventually I figured out how to write a script to do this and wanted to share it. Please note I'm not a Python programmer nor a HTML/CSS guy so no flaming please!
It started as a sample script from their help site and I adapted it to include attachments and to be stand alone.
First download and install Python3. Then pip install fogbugz and jinja2. Place these two files in your python directory, fillin the variables (including the token half way down the script) and then run backit.
1- backit.py
from fogbugz import FogBugz
import sys
from jinja2 import Template
import os
import urllib
def main():
S_FOGBUGZ_URL = 'https://whatever.fogbugz.com/'
S_EMAIL = 'email'
S_PASSWORD = 'pass'
MAX_BUGS = 9999
TEMPLATE_FILE = 'C:\\Users\\zzz\\AppData\\Local\\Programs\\Python\\Python36\\fbBackupTemplate.html'
fb = FogBugz(S_FOGBUGZ_URL)
fb.logon(S_EMAIL, S_PASSWORD)
resp = fb.search(q='type:"Cases"',
cols='ixBug',
max=MAX_BUGS)
tmpl = Template(open(TEMPLATE_FILE,'r').read())
for case in resp.cases.findAll('case'):
ixBug = int(case['ixBug'])
print(ixBug)
respBug = fb.search(q='%s' % ixBug,cols ='sTitle,sPersonAssignedTo,sProject,sArea,sCategory,sPriority,events')
xmlBug = respBug.cases.findAll('case')[0]
bug = {}
bug['ixBug'] = int(xmlBug['ixBug'])
bug['sTitle'] = xmlBug.sTitle.string if xmlBug.sTitle.string else ''
bug['sPersonAssignedTo'] = xmlBug.sPersonAssignedTo.string if xmlBug.sPersonAssignedTo.string else ''
bug['sProject'] = xmlBug.sProject.string if xmlBug.sProject.string else ''
bug['sArea'] = xmlBug.sArea.string if xmlBug.sArea.string else ''
bug['sCategory'] = xmlBug.sCategory.string if xmlBug.sCategory.string else ''
bug['sPriority'] = xmlBug.sPriority.string if xmlBug.sPriority.string else ''
bug['events'] = []
BACKUP_DIR = 'C:\\Temp\\FBBack'
BACKUP_DIR += '\\' + xmlBug.sProject.string if xmlBug.sProject.string else ''
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
BACKUP_DIR += '\\' + str(xmlBug['ixBug'])
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
for event in xmlBug.events.findAll('event'):
bugEvent = {}
bugEvent['ixBugEvent'] = int(event['ixBugEvent'])
bugEvent['sVerb'] = event.sVerb.string if event.sVerb.string else ''
bugEvent['dt'] = event.dt.string if event.dt.string else ''
bugEvent['s'] = event.s.string if event.s.string else ''
bugEvent['sChanges'] = event.sChanges.string if event.sChanges.string else ''
bugEvent['evtDescription'] = event.evtDescription.string if event.evtDescription.string else ''
bugEvent['sPerson'] = event.sPerson.string if event.sPerson.string else ''
bugEvent['s'] = event.s.string.encode('ascii', 'ignore').decode('utf-8') if event.s.string else ''
theAttachments = ''
for att in event.rgAttachments:
theAttachments += 'Attachment: ' +att.sFileName.string + '\n'
print('Downloading attachment: ' + att.sFileName.string)
str1 = att.sURL.string
str2 = 'ixAttachment='
loc1 = str1.find(str2) + len(str2)
loc2 = str1.find('&sFileName')
str3 = ';sFileName='
loc3 = str1.find(str3) + len(str3)
loc4 = str1.find('&sTicket')
theURL = S_FOGBUGZ_URL #+ att.sURL.string
theURL += 'default.asp?'
theURL += 'ixAttachment=' + str1[loc1:loc2]
theURL += '&pg=pgDownload&pgType=pgFile&sFilename=' + str1[loc3:loc4]
theURL += '&token=123456sometoken'
#fix the replace
newFileName = att.sFileName.string.replace('\\','')
newFileName = newFileName.replace(':','')
newFileName = BACKUP_DIR+'\\'+newFileName
#print(newFileName)
urllib.request.urlretrieve(theURL, newFileName)
bugEvent['attachment'] = theAttachments
bug['events'].append(bugEvent)
f = open('%s\\%s.html' % (BACKUP_DIR,bug['ixBug']),'w')
f.write(tmpl.render(bug=bug))
f.close()
fb.view(ixBug=ixBug)
main()
2- fbBackupTemplate.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{bug.ixBug|e}} | {{bug.sTitle|e}}</title>
<style type="text/css">
.bugHeader {
outline-width: 2px;
outline-style: solid;
width: 90%;
padding: 5px;
background: #B8DDFF;
font: 24px "Trebuchet MS", Arial, Helvetica, sans-serif;
}
.bugEvent {
outline-width: 2px;
outline-style: solid;
outline-color: blue;
width: 90%;
padding: 5px;
background: #D2E9FF;
font: 12px Arial, Helvetica, sans-serif;
}
pre.s {
white-space: pre-wrap;
}
</style>
</head>
<body>
<div class="bugHeader">
<span class="ixBug">Bug ID: {{bug.ixBug|e}}</span><br>
<span class="sTitle">Title: {{bug.sTitle|e}}</span><br>
<span class="sPersonAssignedTo">Assigned To: {{bug.sPersonAssignedTo|e}}</span><br>
<span class="sProject">Project: {{bug.sProject|e}}</span><br>
<span class="sArea">Area: {{bug.sArea|e}}</span><br>
<span class="sCategory">Category: {{bug.sCategory|e}}</span><br>
<span class="sPriority">Title: {{bug.sPriority|e}}</span><br>
</div>
<div class="bugEvents">
{% for event in bug.events %}
<div class="bugEvent">
<span class="ixBugEvent">BugEvent ID: {{event.ixBugEvent|e}}</span><br>
<span class="dt">Date/Time: {{event.dt|e}}</span><br>
<span class="sVerb">Verb: {{event.sVerb|e}}</span><br>
<span class="evtDescription">Event Description: {{event.evtDescription|e}}</span><br>
<span class="sChanges">Changes: {{event.sChanges|e}}</span><br>
<span class="sPerson">Person: {{event.sPerson|e}}</span><br>
<span class="Text">Text: </span><br>
<pre class="s">{{event.s|e}}</pre><br>
<pre class="s">{{event.attachment|e}}</pre><br>
</div>
{% endfor %}
</div>
</body>
</html>
Original URL for the backup script:
https://developers.fogbugz.com/?W211
Get a token for the API call:
https://support.fogbugz.com/hc/en-us/articles/360011255754-Generating-a-FogBugz-XML-API-Token

Related

Why does my CSS code renders incorrectly on iOS/MacOS?

We developed a web app on Vue 3 and it displays perfectly on every browser except on iOS and Mac OS devices.
This is worse if you're using Safari, however some issues occur even on Chrome for Mac.
This problem causes forms to always be shown blank, without displaying a placeholder or user input. It also makes several other elements to misalign.
We even tried making a new form and removing all stylesheets, but the problem persists.
Here is the code for one of the forms:
<template>
<div>
<form class="contactus" #submit.prevent="saveMessage">
<div class="row">
<h1 class="titleFormAbout">
{{ $t("contactus.componentForm.text1") }}
</h1>
</div>
<p style="margin-bottom: 30px;">{{ $t("contactus.componentForm.text9") }}</p>
<input type="text" class="form-control formContact" :placeholder="$t('contactus.componentForm.text2')" id="name" required />
<input type="email" class="form-control formContact" :placeholder="$t('contactus.componentForm.text3')" id="email" required />
<vue-tel-input
v-model="phone"
id="phone"
class="formControl formContact"
></vue-tel-input>
<br />
<textarea
class="form-control formContact"
name=""
id="message"
rows="10"
:placeholder="$t('contactus.componentForm.text5')"
required
></textarea>
<br />
<vue-recaptcha
style="margin-bottom: 14px"
#validate="validated"
/>
<div class="row">
<div class="col-6">
<input
class="form-control "
type="Submit"
id="submitButton"
:value="$t('contactus.componentForm.text6')"
disabled
/>
</div>
<div class="col-6">
<button class="form-control clear" #click="clearForm" >
{{ $t("contactus.buttonClear") }}
</button>
</div>
</div>
<br />
<div class="alert" id="Response" role="alert"></div>
</form>
</div>
</template>
<script>
import axios from "axios";
import VueRecaptcha from "../forms/vue-recaptcha.vue";
const hostName = location.port =="" ?location.protocol + "//" + location.host:'http://'+location.hostname+':3000';
export default {
components: {
VueRecaptcha,
},
data() {
return {
config: {
headers: {
//'Content-Type': 'application/x-www-form-urlencoded'
"Content-Type": "multipart/form-data",
},
},
};
},
methods: {
validated(){
const htmlElement=document.getElementById('submitButton');
htmlElement.classList.add('submit');
htmlElement.disabled=false;
},
clearForm() {
document.getElementById("name").value = "";
document.getElementById("email").value = "";
document.getElementsByName("telephone")[0].value = "";
document.getElementById("message").value= "";
},
saveMessage() {
//alert(document.getElementsByClassName("highlighted")[0].getElementsByTagName('span')[0].innerHTML)
const responseBox = document.getElementById("Response");
const params = new FormData();
const countryPhone = document.getElementsByClassName("highlighted");
var countryPhoneid = "+52";
if (countryPhone.length > 0) {
countryPhoneid =
countryPhone[0].getElementsByTagName("span")[0].innerHTML;
}
params.append("name", document.getElementById("name").value);
params.append("email", document.getElementById("email").value);
params.append("phone",countryPhoneid + " " + document.getElementsByName("telephone")[0].value);
params.append("message", document.getElementById("message").value);
axios
.post(hostName + "/api/contact", params, this.config)
.then((response) => {
responseBox.innerHTML = this.$t("contactus.componentForm.text7");
responseBox.classList.add("alert-success");
setTimeout(() => {
responseBox.classList.remove("alert-success");
}, 5000);
console.log(response);
})
.catch((err) => {
responseBox.innerHTML = this.$t("contactus.componentForm.text8");
responseBox.classList.add("alert-danger");
setTimeout(() => {
responseBox.classList.remove("alert-danger");
}, 5000);
console.log(err);
});
responseBox.hidden = false;
setTimeout(() => {
responseBox.hidden = true;
}, 5000);
document.getElementById("name").value = "";
document.getElementById("email").value = "";
document.getElementsByName("telephone")[0].value = "";
document.getElementById("message").value = "";
},
},
mounted() {
document.getElementsByName("telephone")[0].placeholder=this.$t('contactus.componentForm.text10');
const listcountry = document.getElementsByClassName("vti__dropdown-list");
listcountry[0].getElementsByTagName("li")[137].classList.add("highlighted");
document.getElementsByName("telephone")[0].required = true;
document
.getElementsByName("telephone")[0]
.addEventListener("input", (textin) => {
const telephone = document.getElementsByName("telephone")[0];
//console.log(parseInt(textin.data));
//console.log(Number.isInteger(textin.data));<
if (Number.isInteger(parseInt(textin.data))) {
// console.log(telephone.value);
} else {
telephone.value = telephone.value.slice(
0,
telephone.value.length - 1
);
}
});
},
};
</script>
<style>
.contactus .submit {
background: #008b9e;
color: white;
font-weight: bold;
}
.contactus .formContact{
border: 2px solid #BAD1FF;
margin-top: 2vh;
margin-bottom: 2vh;
}
.contactus .clear {
background: rgba(0, 0, 0, 0.25);
font-weight: bold;
}
.contactus .titleFormAbout {
margin-top: 20px;
margin-bottom: 20px;
text-align: left;
color: #00a8c6;
font-weight: bolder;
margin-left: 12px;
text-transform: none;
}
form.contactus {
padding-left: 5vw;
padding-right: 5vw;
}
.contactus form li {
color: black;
}
.contactus .alet {
text-align: center;
}
.contactus .contactus p{
text-align: justify;
}
</style>
Here you can see the type of behaviour we're getting:
On iOS/MacOS:
The same elements on Windows/Android devices:
Can anyone please tell me if this is a common issue and if there's a solution? We have been stuck with this problem for two weeks and we're losing our minds.
Thanks.

jQuery Droppable: Only allow element to drop, if there is no other element in that dropzone

I'm writing a puzzle, where you have to drag an item into the correct dropzone.
Problem: I want that you can only drag an item into a dropzone, if that dropzone does not contain any other items. How can I check, whether there are no other items in that dropzone?
Here is a gif of my current puzzle:
Here is a gif which shows the problem:
As you can see, I can drag multiple items into the same dropzone.
If a dropzone already contains an item, the user should not be able to drop another item into that dropzone. How do I achieve that?
My script so far:
$( ".draggable" ).draggable({ revert: 'invalid', snap: ".dropfield", snapTolerance: 30, snapMode: "inner"});
$( ".dropfield" ).droppable({
accept: ".dropling",
drop: function( event, ui ) {
if(some-condition){ // if correct word got dragged into the correct dropzone
var id = ui.draggable.attr('id');
$("#" + id).draggable( 'disable' );
$(this).droppable( 'disable' );
$("#" + id).css( "background-color", "#7FFF00");
}
});
Html-excerpt:
<div id="liebe" class="dropling draggable text-center">
Liebe
</div>
<span class="dropfield" value="scheitern">
</span>
PS: There are already several topics on Stack-Overflow with the same question. However, I'm not intelligent enough to apply the suggested answers to my case. Please help me.
Edit1
Here is a gif which shows my preferred behavior:
I dragged a wrong word into a dropzone. But as long that dropzone is occupied by a word, no other words should be able to be dropped into that dropzone.
My current code:
if(some-condition){ //correct word
$("#" + id).draggable( 'disable' );
$(this).droppable( 'disable' );
$("#" + id).css( "background-color", "#7FFF00");
}
} else { //wrong word
console.log("wrong word dropped");
$(this).droppable( 'disable' );
}
As soon as I drag the wrong word out of the dropzone, the dropzone should become enabled again. But how can I achieve that?
I would advise breaking this into their own functions. This way you can enable and disable drop repeatedly. Not sure what you want to trigger the item to become draggable and droppable again based on the example you have supplied. Based on what you have supplied, I can offer this the following example.
$(function() {
function enableDrop($target) {
console.log("Enabled Drop");
$target.droppable({
accept: ".dropling",
classes: {
"ui-droppable-hover": "drop-target"
},
drop: function(event, ui) {
var $that = $(this),
dragWord = ui.draggable.text().trim(),
$item = ui.draggable;
if (checkWord(dragWord)) {
console.log("Accepted: " + $item.attr("id"));
$item.
removeClass("draggable")
.draggable('disable')
.attr("style", "")
.appendTo($that);
disableDrop($that);
$that.css("background-color", "#7FFF00");
} else {
return false;
}
}
});
}
function disableDrop($target) {
console.log("Disabling Drop on " + $target.attr("class"));
$target.droppable("destroy");
}
function checkWord(w) {
var result = false;
console.log("Checked Word: " + w);
if (w == "Liebe") {
result = true;
}
return result;
}
$(".draggable").draggable({
revert: 'valid',
snap: ".dropfield",
snapTolerance: 30,
snapMode: "inner"
});
enableDrop($(".dropfield"));
});
p .dropfield {
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
width: 4em;
height: 1.5em;
margin-bottom: -.25em
}
p .drop-target {
border: 1px dashed #ccc;
background-color: #ccc;
}
.text-center {
text-align: center;
}
.draggable {
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
width: 4em;
height: 1em;
padding: .25em 0;
margin-bottom: -.25em
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<p>Diese Schlussfolgerung ist <span class="dropfield" value="scheitern"></span>: Ee kann doch nicht sein, dass es gut ist, </p>
<div id="liebe" class="dropling draggable text-center">Liebe</div>
<div id="absurd" class="dropling draggable text-center">absurd</div>
The easiest way here is probably to solve the whole thing in a more generic way. For this I would add an attribute to the respective Dom element (data-count) and then check how many characters are contained and how many are still allowed:
See /** ADDED **/ for the things i did:
$(function() {
function textWrapper(str, sp, btn) {
if (sp == undefined) {
sp = [0, 0];
}
var txt = "";
if (btn) {
txt = "<span class='w b'>" + str + "</span>";
} else {
txt = "<span class='w'>" + str + "</span>";
}
if (sp[0]) {
txt = " " + txt;
}
if (sp[1]) {
txt = txt + " ";
}
return txt;
}
function chunkWords(p) {
var words = p.split(" ");
words[0] = textWrapper(words[0], [0, 1]);
var i;
for (i = 1; i < words.length; i++) {
var re = /\[.+\]/;
if (re.test(words[i])) {
var b = makeTextBox(words[i].slice(1, -1));
words[i] = " " + b.prop("outerHTML") + " ";
} else {
if (words[0].indexOf(".")) {
words[i] = textWrapper(words[i], [1, 0]);
} else {
words[i] = textWrapper(words[i], [1, 1]);
}
}
}
return words.join("");
}
function unChunkWords(tObj) {
var words = "";
$(tObj).contents().each(function(i, el) {
if ($(el).hasClass("b")) {
words += "[" + $(el).text() + "]";
} else {
words += $(el).text();
}
});
return words.replace(/\s+/g, " ").trim();
}
function makeBtn(tObj) {
var btn = $("<span>", {
class: "ui-icon ui-icon-close"
}).appendTo(tObj);
}
function makeTextBox(txt) {
var sp = $("<span>", {
class: "w b"
}).html(txt);
makeBtn(sp);
return sp;
}
function makeDropText(obj) {
return obj.droppable({
drop: function(e, ui) {
var txt = ui.draggable.text();
var newSpan = textWrapper(txt, [1, 0], 1);
$(this).after(newSpan);
makeBtn($(this).next("span.w"));
makeDropText($(this).next("span.w"));
$("span.w.ui-state-highlight").removeClass("ui-state-highlight");
update()
},
over: function(e, ui) {
$(this).add($(this).next("span.w")).addClass("ui-state-highlight");
},
out: function() {
$(this).add($(this).next("span.w")).removeClass("ui-state-highlight");
}
});
}
$("p.given").html(chunkWords($("p.given").text()));
$("p.given").on("click", ".b > .ui-icon", function() {
$(this).parent().remove();
});
$("p.given").blur(function() {
var w = unChunkWords($(this));
$(this).html(chunkWords(w));
makeDropText($("p.given span.w"));
});
$("span.given").draggable({
helper: "clone",
revert: "invalid"
});
makeDropText($("p.given span.w"));
/** ADDED **/
// update at beginning
update();
// register update events
$("p.given").on('click keydown keyup drag drop', update);
function update(e) {
var templateText = unChunkWords($("p.given"));
var templateTextWithoutParameters = templateText.replace(/\[(.+?)\]/g, "");
var templateTextWithoutParametersLenght = templateTextWithoutParameters.length;
// calc total length
var totalLength = templateTextWithoutParametersLenght;
// since 'helper: clone' we have to ignore it!
$("[data-count]:not(.ui-draggable-dragging)").each(function(index, item) {
var count = $(item).attr("data-count")
var text = "[" + $(item).text() + "]";
var length = templateText.split(text).length - 1;
totalLength += count * length;
});
// 46,8 keycodes for delete & backspace
var maxLength = 200;
if (totalLength >= maxLength && e && e.keyCode !== 46 && e.keyCode !== 8) {
e.preventDefault();
}
// disable data counts
var remaining = maxLength - totalLength;
$("[data-count]:not(.ui-draggable-dragging)").each(function(index, item) {
var count = $(item).attr("data-count");
if (parseInt(count) > remaining) {
$(item).attr("disabled", true);
$(item).draggable().draggable('disable');
} else {
$(item).attr("disabled", false);
$(item).draggable().draggable('enable');
}
})
$(".output").text(totalLength);
}
});
p.given {
display: flex;
flex-wrap: wrap;
}
p.given span.w span.ui-icon {
cursor: pointer;
}
div.blanks {
display: inline-block;
min-width: 50px;
border-bottom: 2px solid #000000;
color: #000000;
}
div.blanks.ui-droppable-active {
min-height: 20px;
}
span.answers>b {
border-bottom: 2px solid #000000;
}
span.given {
margin: 5px;
}
/** ADDED **/
[disabled] {
color: grey
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div class="row">
<p class="given" contenteditable="true">Lorem Ipsum is [Test] Ipsum has been the industry's [America] standard dummy text ever since the 1500s, </p>
</div>
<div class="divider"></div>
<div class="section">
<section>
<div class="card blue-grey ">
<div class="card-content white-text">
<div class="row">
<div class="col s12">
<span class="given btn-flat white-text red lighten-1" rel="1" data-count="50">Test</span>
<span class="given btn-flat white-text red lighten-1" rel="2" data-count="30">America</span>
<span class="given btn-flat white-text red lighten-1" rel="3" data-count="20">Qatar</span>
<span class="given btn-flat white-text red lighten-1" rel="4" data-count="10">Philippines</span>
</div>
</div>
</div>
</div>
</section>
</div>
<div class="divider"></div>
Count: <span class="output"></span>

OSM building and jQuery mobile

I have a problem with input "range" and OSM Buildings for leaflet maps. I used basic example from official example which works fine but if I add jquery mobile to <head> section it breaks the range input. It's strange... I was using jquery mobile input to change the leaflet map opacity and it's working. Here is my basic example:
var map = new L.Map('map');
map.setView([52.52111, 13.40988], 16, false);
new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'osm.org',
maxZoom: 18,
maxNativeZoom: 20
}).addTo(map);
var osmb = new OSMBuildings(map).load();
//************************************************************
var now,
date, time,
timeRange, dateRange,
timeRangeLabel, dateRangeLabel;
function changeDate() {
var Y = now.getFullYear(),
M = now.getMonth(),
D = now.getDate(),
h = now.getHours(),
m = 0;
timeRangeLabel.innerText = pad(h) + ':' + pad(m);
dateRangeLabel.innerText = Y + '-' + pad(M+1) + '-' + pad(D);
osmb.date(new Date(Y, M, D, h, m));
}
function onTimeChange() {
now.setHours(this.value);
now.setMinutes(0);
changeDate();
}
function onDateChange() {
now.setMonth(0);
now.setDate(this.value);
changeDate();
}
function pad(v) {
return (v < 10 ? '0' : '') + v;
}
timeRange = document.getElementById('time');
dateRange = document.getElementById('date');
timeRangeLabel = document.querySelector('*[for=time]');
dateRangeLabel = document.querySelector('*[for=date]');
now = new Date;
changeDate();
// init with day of year
var Jan1 = new Date(now.getFullYear(), 0, 1);
dateRange.value = Math.ceil((now-Jan1)/86400000);
timeRange.value = now.getHours();
timeRange.addEventListener('change', onTimeChange);
dateRange.addEventListener('change', onDateChange );
timeRange.addEventListener('input', onTimeChange);
dateRange.addEventListener('input', onDateChange);
body {
font-family: sans-serif;
padding: 5px;
margin: 0;
}
#map {
width: 300px;
height: 300px;
float: left;
margin: 0 15px 0 0;
}
label {
height: 20px;
}
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>Shadows</title>
<script src='http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js'></script>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script src='http://cdn.osmbuildings.org/OSMBuildings-Leaflet.js'></script>
<link rel='stylesheet prefetch' href='http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css'>
</head>
<body>
<div id="map"></div>
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<input id="date" type="range" min="1" max="365" step="1"><label for="date"></label><br>
<input id="time" type="range" min="0" max="23" step="1"><label for="time"></label>
</body>
</html>
For jQuery Mobile rangesliders you may use:
$(document).on("pagecreate", "#page-1", function() {
$("#date").on("change", onDateChange);
$("#time").on("change", onTimeChange);
});
Codepen: http://codepen.io/anon/pen/gLJRXb

tablesorter pager (v 2.10) with page numbers

The latest version of the tablesorter pager plugin seems to be missing page number support and # of items per page. With the older version (v2.0), it was possible to do so. The reason for asking this is because we need to take advantage of the ajax fetching of the rows, introduced in the newer versions (since fetching all the data at once is causing a performance hit) while keeping the look and feel of the table same as before. However, a lot has changed from v2.0 to v2.10. I also couldn't find any examples of modifying the updatePageDisplay function that would help in achieving this.
The image below shows what I'm trying to accomplish:
Thanks in advance.
The latest version is much more flexible than the original. So, if we start with this pager HTML (page size numbers reduced to match this demo; also notice the second pager block at the top only showing the visible and total record numbers)
<div class="pager">
<span class="left">
# per page:
5 |
10 |
20 |
50
</span>
<span class="right">
<span class="prev">
<img src="http://mottie.github.com/tablesorter/addons/pager/icons/prev.png" /> Prev
</span>
<span class="pagecount"></span>
<span class="next">Next
<img src="http://mottie.github.com/tablesorter/addons/pager/icons/next.png" />
</span>
</span>
this css
.left { float: left; }
.right {
float: right;
-webkit-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
}
.pager .prev, .pager .next, .pagecount { cursor: pointer; }
.pager a {
color: black;
}
.pager a.current {
text-decoration: none;
color: #0080ff;
}
and this script
var $table = $('table')
.on('pagerInitialized pagerComplete', function (e, c) {
var i, pages = '', t = [],
cur = c.page + 1,
start = cur > 1 ? (c.totalPages - cur < 3 ? -3 + (c.totalPages - cur) : -1) : 0,
end = cur < 3 ? 5 - cur : 2;
for (i = start; i < end; i++) {
if (cur + i >= 1 && cur + i < c.totalPages) { t.push( cur + i ); }
}
// make sure first and last page are included in the pagination
if ($.inArray(1, t) === -1) { t.push(1); }
if ($.inArray(c.totalPages, t) === -1) { t.push(c.totalPages); }
// sort the list
t = t.sort(function(a, b){ return a - b; });
// make links and spacers
$.each(t, function(j, v){
pages += '' + v + '';
pages += j < t.length - 1 && ( t[j+1] - 1 !== v ) ? ' ... ' : ( j >= t.length - 1 ? '' : ' | ' );
});
$('.pagecount').html(pages);
})
.tablesorter({
theme: 'blackice',
widgets: ['zebra', 'columns']
})
.tablesorterPager({
// target the pager markup - see the HTML block below
container: $(".pager"),
size: 5,
output: 'showing: {startRow} to {endRow} ({totalRows})'
});
// set up pager controls
$('.pager .left a').on('click', function () {
$(this)
.addClass('current')
.siblings()
.removeClass('current');
$table.trigger('pageSize', $(this).html());
return false;
});
$('.pager .right .pagecount').on('click', 'a', function(){
$(this)
.addClass('current')
.siblings()
.removeClass('current');
$table.trigger('pageSet', $(this).html());
return false;
});
please see the live demo and download the customized code.Using this extended version you can add multiple tables in one page.
http://www.pearlbells.co.uk/table-pagination/

jquery ui sortables connect lists: copy items

I have two lists, I want both of them to be sortable and want to be able to copy (drag) items from list1 to list2 and vice versa.
http://jqueryui.com/demos/sortable/#connect-lists
Is what I want, but the items are moved, not copied.
I did a few experiments with draggables and droppables, but I can't get to to work keeping them sortable. For example: http://jsfiddle.net/tunafish/dvXmf/
OK here is my app; two lists of images, sortable and you can copy over from the connected list.
If an item already exists in the target it's disabled.
Hopefully useful to someone...
JSFiffle here: http://jsfiddle.net/tunafish/VBG5V/
CSS:
.page { width: 410px; padding: 20px; margin: 0 auto; background: darkgray; }
.album { list-style: none; overflow: hidden;
width: 410px; margin: 0; padding: 0; padding-top: 5px;
background: gray;
}
.listing { margin-bottom: 10px; }
.album li { float: left; outline: none;
width: 120px; height: 80px; margin: 0 0 5px 5px; padding: 5px;
background: #222222;
}
li.placeholder { background: lightgray; }
JS:
$("ul, li").disableSelection();
$(".album, .favorites").sortable({
connectWith: ".album, .favorites",
placeholder: "placeholder",
forcePlaceholderSize: true,
revert: 300,
helper: "clone",
stop: uiStop,
receive: uiReceive,
over: uiOver
});
$(".album li").mousedown(mStart);
var iSender, iTarget, iIndex, iId, iSrc, iCopy;
var overCount = 0;
/* everything starts here */
function mStart() {
// remove any remaining .copy classes
$(iSender + " li").removeClass("copy");
// set vars
if ($(this).parent().hasClass("listing")) { iSender = ".listing"; iTarget = ".favorites"; }
else { iSender = ".favorites"; iTarget = ".listing"; }
iIndex = $(this).index();
iId = $(this).attr("id");
iSrc = $(this).find("img").attr("src");
iCopy = $(iTarget + " li img[src*='" + iSrc + "']").length > 0; // boolean, true if there is allready a copy in the target list
// disable target if item is allready in there
if (iCopy) { $(iTarget).css("opacity","0.5").sortable("disable"); }
}
/* when sorting has stopped */
function uiStop(event, ui) {
// enable target
$(iTarget).css("opacity","1.0").sortable("enable");
// reset item vars
iSender = iTarget = iIndex = iId = iSrc = iCopy = undefined;
overCount = 0;
// reinit mousedown, live() did not work to disable
$(".album li").mousedown(mStart);
}
/* rolling over the receiver - over, out, over etc. */
function uiOver(event, ui) {
// only if item in not allready in the target
if (!iCopy) {
// counter for over/out (numbers even/uneven)
overCount++;
// if even [over], clone to original index in sender, show and fadein (sortables hides it)
if (overCount%2) {
if (iIndex == 0) { ui.item.clone().addClass("copy").attr("id", iId).prependTo(iSender).fadeIn("slow"); }
else { ui.item.clone().addClass("copy").attr("id", iId).insertAfter(iSender + " li:eq(" + iIndex + ")").fadeIn("slow"); }
}
// else uneven [out], remove copies
else { $(iSender + " li.copy").remove(); }
}
// else whoooooo
}
/* list transfers, fix ID's here */
function uiReceive(event, ui) {
(iTarget == ".favorites") ? liPrefix = "fli-" : liPrefix = "lli-";
// set ID with index for each matched element
$(iTarget + " li").each(function(index) {
$(this).attr("id", liPrefix + (index + 1)); // id's start from 1
});
}
HTML:
<div class="page">
<div class="container">
<h2>Photo Album</h2>
<ul class="listing album">
<li id="li-1"><img src="tn/001.jpg" /></li>
<li id="li-2"><img src="tn/002.jpg" /></li>
<li id="li-3"><img src="tn/003.jpg" /></li>
<li id="li-4"><img src="tn/004.jpg" /></li>
<li id="li-5"><img src="tn/005.jpg" /></li>
</ul>
</div>
<div style="clear:both;"></div>
<div class="container">
<h2>Favorites</h2>
<ul class="favorites album">
<li id="fli-1"><img src="tn/001.jpg" /></li>
<li id="fli-2"><img src="tn/002.jpg" /></li>
<li id="fli-3"><img src="tn/010.jpg" /></li>
</ul>
</div>
</div>
I have to say that FFish's answer to this has been incredibly helpful to me.
I'd make one suggestion; if the lists are being constantly changed the mousedown event seem to be called numerous times because of the re-registering of the event on all the child objects. It might be a bit of a kludge, but I've added an unbind first to ensure the mousedown event is only called once.
$(".album li").mousedown(mStart);
to
$(".album li").unbind('mousedown', mStart).mousedown(mStart);

Resources