mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
added gen4 ui prototype...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
ece95ff30f
commit
a458b592e7
249
ui (gen4)/Makefile
Executable file
249
ui (gen4)/Makefile
Executable file
@ -0,0 +1,249 @@
|
||||
#**********************************************************************
|
||||
# TODO: build to a BUILD_DIR...
|
||||
# TODO: build all target platforms...
|
||||
# - Windows (AppJS)
|
||||
# - MacOSX (AppJS)
|
||||
# - Windows8 (native?) XXX
|
||||
# - PhoneGap-remote
|
||||
# push and api call to fetch and rebuild
|
||||
# - PhoneGap-local XXX
|
||||
#
|
||||
|
||||
APP_NAME=ImageGrid.Viewer
|
||||
|
||||
|
||||
# process LESS files to CSS...
|
||||
%.css: %.less
|
||||
lessc $< > $@
|
||||
|
||||
# minify js...
|
||||
%.min.js: %.js
|
||||
uglifyjs $< -c -o $@
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
|
||||
# get all the .less files to process...
|
||||
CSS_FILES := $(patsubst %.less,%.css,$(wildcard *.less))
|
||||
|
||||
LIB_DIR=lib
|
||||
EXT_LIB_DIR=ext-lib
|
||||
CSS_DIR=css
|
||||
NW_PROJECT_FILE=package.json
|
||||
JS_FILES := $(wildcard *.js)
|
||||
HTML_FILES := $(wildcard *.html)
|
||||
|
||||
# get files to minify...
|
||||
JS_MIN_FILES := $(patsubst %.js,%.min.js,$(wildcard *.js))
|
||||
|
||||
LOGS := *.log
|
||||
|
||||
NODE_DIR=node_modules
|
||||
BUILD_DIR=build
|
||||
WIN_BUILD_DIR=build/Win32
|
||||
MAC_BUILD_DIR=build/MacOSX
|
||||
MAC_10_6_BUILD_DIR=build/MacOSX-10.6
|
||||
LINUX_IA32_BUILD_DIR=build/Linux-ia32
|
||||
LINUX_X64_BUILD_DIR=build/Linux-x64
|
||||
ANDROID_BUILD_DIR=build/Android
|
||||
IOS_BUILD_DIR=build/iOS
|
||||
|
||||
DIST_DIR=dist
|
||||
|
||||
# XXX add version
|
||||
WIN_DIST_ZIP=$(DIST_DIR)/$(APP_NAME)-win32.zip
|
||||
MAC_DIST_ZIP=$(DIST_DIR)/$(APP_NAME)-osx.zip
|
||||
MAC_10_6_DIST_ZIP=$(DIST_DIR)/$(APP_NAME)-osx10.6.zip
|
||||
|
||||
|
||||
APP_ZIP=$(BUILD_DIR)/app.zip
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
|
||||
all: dev
|
||||
|
||||
|
||||
minify: $(JS_MIN_FILES)
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
# build dependencies...
|
||||
# XXX can make auto-create directories???
|
||||
|
||||
$(NODE_DIR):
|
||||
mkdir -p $(NODE_DIR)
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
$(WIN_BUILD_DIR):
|
||||
mkdir -p $(WIN_BUILD_DIR)
|
||||
$(MAC_BUILD_DIR):
|
||||
mkdir -p $(MAC_BUILD_DIR)
|
||||
$(MAC_10_6_BUILD_DIR):
|
||||
mkdir -p $(MAC_10_6_BUILD_DIR)
|
||||
$(LINUX_IA32_BUILD_DIR):
|
||||
mkdir -p $(LINUX_IA32_BUILD_DIR)
|
||||
$(LINUX_X64_BUILD_DIR):
|
||||
mkdir -p $(LINUX_X64_BUILD_DIR)
|
||||
$(ANDROID_BUILD_DIR):
|
||||
mkdir -p $(ANDROID_BUILD_DIR)
|
||||
$(IOS_BUILD_DIR):
|
||||
mkdir -p $(IOS_BUILD_DIR)
|
||||
|
||||
$(DIST_DIR):
|
||||
mkdir -p $(DIST_DIR)
|
||||
|
||||
|
||||
$(APP_ZIP): $(CSS_FILES) $(BUILD_DIR) $(NODE_DIR) node-deps
|
||||
zip -r $(APP_ZIP) $(NW_PROJECT_FILE) $(JS_FILES) $(CSS_FILES) \
|
||||
$(HTML_FILES) $(LIB_DIR) $(EXT_LIB_DIR) $(CSS_DIR) \
|
||||
$(NODE_DIR)
|
||||
|
||||
zip: $(APP_ZIP)
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
# dev env...
|
||||
|
||||
node-deps:
|
||||
npm install fs.extra
|
||||
# npm install exif
|
||||
|
||||
dev: $(CSS_FILES)
|
||||
unzip -uj $(wildcard targets/node-webkit/node-webkit-*-win-ia32.zip) -d .
|
||||
rm -f nwsnapshot.exe credits.html
|
||||
chmod +x *.{exe,dll}
|
||||
|
||||
#dev-targets:
|
||||
# mkdir -p targets/node-webkit
|
||||
# wget
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
# build targets...
|
||||
# XXX most of the code here is duplicated, find a way to reuse sections...
|
||||
|
||||
# node-webkit win32
|
||||
win32: $(APP_ZIP) $(WIN_BUILD_DIR)
|
||||
unzip -uj $(wildcard targets/node-webkit/node-webkit-*-win-ia32.zip) \
|
||||
-d $(WIN_BUILD_DIR)
|
||||
cat $(APP_ZIP) >> $(WIN_BUILD_DIR)/nw.exe
|
||||
mv $(WIN_BUILD_DIR)/nw.exe $(WIN_BUILD_DIR)/$(APP_NAME).exe
|
||||
chmod +x $(WIN_BUILD_DIR)/*.{exe,dll}
|
||||
rm -f $(WIN_BUILD_DIR)/nwsnapshot.exe \
|
||||
$(WIN_BUILD_DIR)/credits.html
|
||||
|
||||
win32-dist: win32 $(DIST_DIR)
|
||||
# XXX include vips...
|
||||
# XXX build and include gid, buldcache...
|
||||
# XXX include scripts/utils...
|
||||
zip -r $(WIN_DIST_ZIP) $(WIN_BUILD_DIR)
|
||||
|
||||
|
||||
|
||||
# node-webkit mac
|
||||
# XXX BUG: rebuilding without cleaning will mess up folders...
|
||||
# XXX this is for 10.7+
|
||||
osx: $(APP_ZIP) $(MAC_BUILD_DIR) Info.plist
|
||||
unzip -u $(wildcard targets/node-webkit/node-webkit-*-osx-ia32.zip) \
|
||||
-d $(MAC_BUILD_DIR)
|
||||
cp $(APP_ZIP) $(MAC_BUILD_DIR)/node-webkit.app/Contents/Resources/app.nw
|
||||
# XXX not sure if this is needed...
|
||||
chmod +x $(MAC_BUILD_DIR)/node-webkit.app/Contents/Resources/app.nw
|
||||
# XXX there is something wrong with the updated Info.plist, need to investigate...
|
||||
cp Info.plist $(MAC_BUILD_DIR)/node-webkit.app/Contents/
|
||||
mv $(MAC_BUILD_DIR)/node-webkit.app $(MAC_BUILD_DIR)/$(APP_NAME).app
|
||||
# XXX TODO: add real credits...
|
||||
rm -f $(MAC_BUILD_DIR)/nwsnapshot \
|
||||
$(MAC_BUILD_DIR)/credits.html
|
||||
|
||||
# XXX this is almost identical to osx...
|
||||
# XXX BUG: rebuilding without cleaning will mess up folders...
|
||||
osx-10.6: $(APP_ZIP) $(MAC_10_6_BUILD_DIR) Info.plist
|
||||
unzip -u $(wildcard targets/node-webkit/node-webkit-*-osx10.6-ia32.zip) \
|
||||
-d $(MAC_10_6_BUILD_DIR)
|
||||
cp $(APP_ZIP) $(MAC_10_6_BUILD_DIR)/node-webkit.app/Contents/Resources/app.nw
|
||||
# XXX not sure if this is needed...
|
||||
chmod +x $(MAC_10_6_BUILD_DIR)/node-webkit.app/Contents/Resources/app.nw
|
||||
# XXX there is something wrong with the updated Info.plist, need to investigate...
|
||||
cp Info.plist $(MAC_10_6_BUILD_DIR)/node-webkit.app/Contents/
|
||||
mv $(MAC_10_6_BUILD_DIR)/node-webkit.app $(MAC_10_6_BUILD_DIR)/$(APP_NAME).app
|
||||
# XXX TODO: add real credits...
|
||||
rm -f $(MAC_10_6_BUILD_DIR)/nwsnapshot \
|
||||
$(MAC_10_6_BUILD_DIR)/credits.html
|
||||
|
||||
|
||||
|
||||
osx-dist: osx $(DIST_DIR)
|
||||
zip -r $(MAC_DIST_ZIP) $(MAC_BUILD_DIR)
|
||||
|
||||
osx-10.6-dist: osx $(DIST_DIR)
|
||||
zip -r $(MAC_10_6_DIST_ZIP) $(MAC_10_6_BUILD_DIR)
|
||||
|
||||
|
||||
|
||||
# node-webkit linux-ia32
|
||||
linux-ia32: $(APP_ZIP) $(LINUX_IA32_BUILD_DIR)
|
||||
tar --strip-components 1 \
|
||||
-xzf $(wildcard targets/node-webkit/node-webkit-*-linux-ia32.tar.gz) \
|
||||
-C $(LINUX_IA32_BUILD_DIR)
|
||||
cat $(APP_ZIP) >> $(LINUX_IA32_BUILD_DIR)/nw
|
||||
mv $(LINUX_IA32_BUILD_DIR)/nw $(LINUX_IA32_BUILD_DIR)/$(APP_NAME)
|
||||
chmod +x $(LINUX_IA32_BUILD_DIR)/*
|
||||
rm -f $(LINUX_IA32_BUILD_DIR)/nwsnapshot \
|
||||
$(LINUX_IA32_BUILD_DIR)/credits.html
|
||||
|
||||
linux-ia32-dist: linux-ia32 $(DIST_DIR)
|
||||
# XXX use tar -czf ...
|
||||
zip -r $(LINUX_IA32_BUILD_DIR) $(LINUX_IA32_BUILD_DIR)
|
||||
|
||||
|
||||
|
||||
# node-webkit linux-x64
|
||||
linux-x64: $(APP_ZIP) $(LINUX_X64_BUILD_DIR)
|
||||
tar --strip-components 1 \
|
||||
-xzf $(wildcard targets/node-webkit/node-webkit-*-linux-x64.tar.gz) \
|
||||
-C $(LINUX_X64_BUILD_DIR)
|
||||
cat $(APP_ZIP) >> $(LINUX_X64_BUILD_DIR)/nw
|
||||
mv $(LINUX_X64_BUILD_DIR)/nw $(LINUX_X64_BUILD_DIR)/$(APP_NAME)
|
||||
chmod +x $(LINUX_X64_BUILD_DIR)/*
|
||||
rm -f $(LINUX_X64_BUILD_DIR)/nwsnapshot \
|
||||
$(LINUX_X64_BUILD_DIR)/credits.html
|
||||
|
||||
linux-x64-dist: linux-x64 $(DIST_DIR)
|
||||
# XXX use tar -czf ...
|
||||
zip -r $(LINUX_X64_BUILD_DIR) $(LINUX_X64_BUILD_DIR)
|
||||
|
||||
|
||||
|
||||
# XXX android...
|
||||
# XXX iOS...
|
||||
|
||||
|
||||
all: win32 osx osx-10.6 linux-ia32 linux-x64
|
||||
|
||||
dist: win32-dist osx-dist
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
# cleanup...
|
||||
|
||||
clean-dev:
|
||||
rm -rf *.exe *.dll *.pak
|
||||
|
||||
clean-build:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
clean: clean-build
|
||||
rm -f $(CSS_FILES) $(JS_MIN_FILES) $(LOGS)
|
||||
|
||||
clean-all: clean clean-dev
|
||||
|
||||
|
||||
|
||||
#**********************************************************************
|
||||
280
ui (gen4)/actions.js
Executable file
280
ui (gen4)/actions.js
Executable file
@ -0,0 +1,280 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
//
|
||||
// Might also be a good idea to add "relative terms" to be used as
|
||||
// arguments for actions (a-la jQuery collections):
|
||||
//
|
||||
// Image - current image
|
||||
// Images - all images
|
||||
// Ribbon - ribbon or ribbon images
|
||||
// Marked - marked images
|
||||
// Bookmarked - bookmarked images
|
||||
//
|
||||
// NOTE: these can also beused as a basis for actions...
|
||||
//
|
||||
//
|
||||
/*********************************************************************/
|
||||
|
||||
// NOTE: context is dynamic.
|
||||
function Action(context, name, doc, code){
|
||||
var action = function(){
|
||||
var args = args2array(arguments)
|
||||
var c = $(context)
|
||||
.trigger(name + '.pre', args)
|
||||
|
||||
// run compound action content...
|
||||
if(code != null){
|
||||
// code is a function...
|
||||
if(typeof(code) == typeof(function(){})){
|
||||
code.apply(this, [c].concat(args))
|
||||
|
||||
// code is an object...
|
||||
} else {
|
||||
for(var a in code){
|
||||
var sargs = code[a]
|
||||
sargs = sargs.constructor.name != 'Array' ? [sargs] : sargs
|
||||
this[a].apply(this, sargs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
.trigger(name, args)
|
||||
.trigger(name + '.post', args)
|
||||
}
|
||||
action.doc = doc == null ? name : doc
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
// if actions is given this will extend that action object, else a new
|
||||
// action object will be created.
|
||||
//
|
||||
// names format:
|
||||
// {
|
||||
// // basic action...
|
||||
// <action-name>: <doc>,
|
||||
//
|
||||
// // compound action...
|
||||
// <action-name>: [<doc>, {
|
||||
// <action-name>: <args>,
|
||||
// ...
|
||||
// }],
|
||||
//
|
||||
// // compound action with a JS function...
|
||||
// <action-name>: [<doc>,
|
||||
// // this is run in the context of the action set...
|
||||
// // NOTE: this will get the same arguments passed to the action
|
||||
// // preceded with the action event context.
|
||||
// function(evt_context, ...){
|
||||
// ...
|
||||
// }],
|
||||
//
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
//
|
||||
// NOTE: context is dynamic.
|
||||
function Actions(context, names, actions){
|
||||
actions = actions == null ? {} : actions
|
||||
Object.keys(names).forEach(function(e){
|
||||
var doc = names[e]
|
||||
var code = doc.constructor.name == 'Array' ? doc[1] : null
|
||||
doc = code != null ? doc : doc[0]
|
||||
|
||||
actions[e] = Action(context, e, doc, code)
|
||||
})
|
||||
return actions
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// XXX need a way to define compound actions...
|
||||
// - compound action is like a normal action with a set of other
|
||||
// actions chanined to it's main event.
|
||||
// - actions should accept arguments, both optional and required
|
||||
var BASE_ACTIONS = {
|
||||
// basic editing...
|
||||
shiftImageUp:
|
||||
'Shift image to the ribbon above current, creating one if '
|
||||
+'it does not exist',
|
||||
shiftImageDown:
|
||||
'Shift image to the ribbon below current, creating one if '
|
||||
+'it does not exist',
|
||||
shiftImageLeft: 'Shift image to the left',
|
||||
shiftImageRight: 'Shift image to the right',
|
||||
|
||||
moveRibbonUp: 'Move current ribbon one position up',
|
||||
moveRibbonDown: 'Move current ribbon one position down',
|
||||
|
||||
sortImages: '',
|
||||
reverseImages: '',
|
||||
setAsBaseRibbon: '',
|
||||
|
||||
// image adjustments...
|
||||
rotateCW: '',
|
||||
rotateCCW: '',
|
||||
flipVertical: '',
|
||||
flipHorizontal: '',
|
||||
|
||||
// external editors/viewers...
|
||||
systemOpen: '',
|
||||
openWith: '',
|
||||
|
||||
// crop...
|
||||
// XXX should this be here on in a crop pligin...
|
||||
cropRibbon: '',
|
||||
uncropView: '',
|
||||
uncropAll: '',
|
||||
|
||||
openURL: '',
|
||||
openHistory: '',
|
||||
|
||||
saveState: '',
|
||||
exportImages: '',
|
||||
|
||||
exit: '',
|
||||
}
|
||||
|
||||
|
||||
// XXX think of a better name...
|
||||
function setupBaseActions(context, actions){
|
||||
return Actions(context, BASE_ACTIONS, actions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
var UI_ACTIONS = {
|
||||
// basic navigation...
|
||||
nextImage: 'Focus next image in current ribbon',
|
||||
nextRibbon: 'Focus next ribbon (down)',
|
||||
nextScreen: 'Show next screen width of images',
|
||||
|
||||
prevImage: 'Focus previous image in current ribbon',
|
||||
prevRibbon: 'Focus previous ribbon (up)',
|
||||
prevScreen: 'Show previous screen width of images',
|
||||
|
||||
firstImage: 'Focus first image in ribbon',
|
||||
lastImage: 'Focus last image in ribbon',
|
||||
|
||||
// zooming...
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
|
||||
// NOTE: if this gets a count argument it will fit count images,
|
||||
// default is one.
|
||||
fitImage: 'Fit image',
|
||||
|
||||
// XXX should these be relative to screen rather than actual image counts?
|
||||
fitTwo: ['Fit two images', { fitImage: 2, }],
|
||||
fitThree: ['Fit three images', { fitImage: 3, }],
|
||||
fitFour: ['Fit four images', { fitImage: 4, }],
|
||||
fitFive: ['Fit five images', { fitImage: 5, }],
|
||||
fitSix: ['Fit six images', { fitImage: 6, }],
|
||||
fitSeven: ['Fit seven images', { fitImage: 7, }],
|
||||
fitEight: ['Fit eight images', { fitImage: 8, }],
|
||||
fitNine: ['Fit nine images', { fitImage: 9, }],
|
||||
|
||||
fitMax: 'Fit the maximum number of images',
|
||||
|
||||
fitSmall: 'Show small image',
|
||||
fitNormal: 'Show normal image',
|
||||
fitScreen: 'Fit image to screen',
|
||||
|
||||
// modes...
|
||||
singleImageMode: '',
|
||||
ribbonMode: '',
|
||||
|
||||
toggleTheme: '',
|
||||
|
||||
// panels...
|
||||
togglePanels: '',
|
||||
showInfoPanel: '',
|
||||
showTagsPanel: '',
|
||||
showSearchPanel: '',
|
||||
showQuickEditPanel: '',
|
||||
showStatesPanel: '',
|
||||
showConsolePanel: '',
|
||||
|
||||
// developer actions...
|
||||
showConsole: '',
|
||||
showDevTools: '',
|
||||
}
|
||||
|
||||
|
||||
// XXX think of a better name...
|
||||
function setupUIActions(context, actions){
|
||||
return Actions(context, UI_ACTIONS, actions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Marks actions...
|
||||
// XXX move to marks.js
|
||||
var MARKS_ACTIONS = {
|
||||
toggleMark: '',
|
||||
toggleMarkBlock: '',
|
||||
|
||||
markRibbon: '',
|
||||
unmarkRibbon: '',
|
||||
markAll: '',
|
||||
unmarkAll: '',
|
||||
invertMarkedRibbon: '',
|
||||
invertMarkedAll: '',
|
||||
|
||||
shiftMarkedUp: '',
|
||||
shiftMarkedDown: '',
|
||||
shiftMarkedLeft: '',
|
||||
shiftMarkedRight: '',
|
||||
|
||||
cropMarkedImages: '',
|
||||
cropMarkedImagesToSingleRibbon: '',
|
||||
}
|
||||
|
||||
function setupMarksActions(context, actions){
|
||||
return Actions(context, MARKS_ACTIONS, actions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Bookmarks actions...
|
||||
// XXX move to bookmarks.js
|
||||
var BOOKMARKS_ACTIONS = {
|
||||
toggleBookmark: 'Toggle image bookmark',
|
||||
|
||||
bookmarkMarked: 'Bookmark marked images',
|
||||
unbookmarkMarked: 'Remove bookmarks from marked images',
|
||||
toggleBookmarkMarked: 'Toggle bookmarks on marked images',
|
||||
|
||||
clearRibbonBookmarks: 'Remove bookmarks in ribbon',
|
||||
clearAllBookmarks: 'Clear all bookmarks',
|
||||
|
||||
cropBookmarkedImages: '',
|
||||
cropBookmarkedImagesToSingleRibbon: '',
|
||||
}
|
||||
|
||||
function setupBookmarksActions(context, actions){
|
||||
return Actions(context, BOOKMARKS_ACTIONS, actions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
398
ui (gen4)/css/editor.css
Executable file
398
ui (gen4)/css/editor.css
Executable file
@ -0,0 +1,398 @@
|
||||
.panel {
|
||||
position: absolute;
|
||||
|
||||
display: inline-block;
|
||||
min-width: 200px;
|
||||
max-width: 450px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
border: solid 2px silver;
|
||||
border-radius: 4px;
|
||||
|
||||
background: white;
|
||||
box-shadow: 5px 5px 30px -5px rgba(0, 0, 0, 0.5);
|
||||
opacity: 0.95;
|
||||
|
||||
overflow: visible;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.panel summary,
|
||||
.sub-panel summary {
|
||||
padding-left: 3px;
|
||||
background: silver
|
||||
}
|
||||
.panel summary::-webkit-details-marker,
|
||||
.sub-panel summary::-webkit-details-marker {
|
||||
color: gray;
|
||||
}
|
||||
.panel .close-button,
|
||||
.sub-panel .close-button {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
cursor: hand;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.panel .close-button:hover,
|
||||
.sub-panel .close-button:hover {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.sub-panel .close-button {
|
||||
right: 8px;
|
||||
}
|
||||
.panel .close-button,
|
||||
.sub-panel .close-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
.panel:hover>summary .close-button,
|
||||
.sub-panel:hover .close-button {
|
||||
visibility: visible;
|
||||
}
|
||||
.panel .panel-content {
|
||||
display: block;
|
||||
|
||||
min-height: 15px;
|
||||
}
|
||||
.sub-panel,
|
||||
.sub-panel button,
|
||||
.sub-panel .state {
|
||||
margin: 1px;
|
||||
font-size: 11px;
|
||||
border: solid 1px #aaa;
|
||||
border-radius: 4px;
|
||||
/* needed for dragging */
|
||||
background: white;
|
||||
}
|
||||
.sub-panel {
|
||||
display: block;
|
||||
margin: 3px;
|
||||
border: solid 1px silver;
|
||||
box-shadow: none;
|
||||
}
|
||||
.sub-panel.blink {
|
||||
box-shadow: 0px 0px 10px 0px rgba(255,0,0,1)
|
||||
}
|
||||
.sub-panel summary {
|
||||
background: #ddd;
|
||||
/*
|
||||
background: white;
|
||||
box-shadow: 0px 0px 50px -5px rgba(0, 0, 0, 0.4);
|
||||
*/
|
||||
}
|
||||
.sub-panel .sub-panel-content {
|
||||
margin: 10px;
|
||||
/*
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
*/
|
||||
}
|
||||
|
||||
.sub-panel button:active,
|
||||
.sub-panel .state:active {
|
||||
background: silver;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
bottom: 0px;
|
||||
min-width: 10px;
|
||||
|
||||
background: white;
|
||||
opacity: 0.95;
|
||||
|
||||
box-shadow: 0px 0px 30px -5px rgba(0, 0, 0, 0.3);
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.side-panel:not(:empty):hover:after {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
content: "Double click to toggle auto-hide (now: " attr(autohide) ")";
|
||||
color: gray;
|
||||
|
||||
font-size: 10px;
|
||||
padding: 5px;
|
||||
bottom: 0px;
|
||||
|
||||
opacity: 0.5;
|
||||
}
|
||||
.side-panel.right:not(:empty):after {
|
||||
right: 0px;
|
||||
}
|
||||
.side-panel[open],
|
||||
.side-panel:not(:empty)[autohide=off],
|
||||
.side-panel[autohide=on]:not(:empty):hover {
|
||||
min-width: 200px;
|
||||
}
|
||||
.side-panel.left {
|
||||
left: 0px;
|
||||
border-right: solid 1px silver;
|
||||
}
|
||||
.side-panel.right {
|
||||
right: 0px;
|
||||
border-left: solid 1px silver;
|
||||
}
|
||||
|
||||
.side-panel[autohide=on] .sub-panel {
|
||||
display: none;
|
||||
}
|
||||
.side-panel[open] .sub-panel,
|
||||
.side-panel[autohide=on]:hover .sub-panel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* main controls */
|
||||
.sub-panel .control {
|
||||
white-space:nowrap;
|
||||
}
|
||||
.sub-panel .control .title {
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
cursor: move;
|
||||
}
|
||||
.sub-panel .control .slider {
|
||||
-webkit-appearance: none !important;
|
||||
width: 150px;
|
||||
height: 3px;
|
||||
border: solid 1px #ccc;
|
||||
border-radius: 2px;
|
||||
background: white;
|
||||
}
|
||||
.sub-panel .control.at-default .slider {
|
||||
}
|
||||
.sub-panel .control .slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none !important;
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
/*border: solid 1px gray;*/
|
||||
border: solid 2px #aaa;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
box-shadow: 1px 1px 10px 0px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.sub-panel .control.at-default .slider::-webkit-slider-thumb {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sub-panel .control .value {
|
||||
-webkit-appearance: none !important;
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
.sub-panel .control input::-webkit-outer-spin-button,
|
||||
.sub-panel .control input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
.sub-panel .control .reset {
|
||||
visibility: hidden;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
.sub-panel .control:hover button.reset {
|
||||
visibility: visible;
|
||||
}
|
||||
.sub-panel .control .reset:hover {
|
||||
border: solid 1px silver;
|
||||
}
|
||||
|
||||
|
||||
/* Snapshots */
|
||||
.sub-panel .state {
|
||||
display: inline-block;
|
||||
margin: 1px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.sub-panel .state.ui-draggable-dragging {
|
||||
box-shadow: 2px 2px 10px -2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.sub-panel .states {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
|
||||
/* misc */
|
||||
.sub-panel hr {
|
||||
border: none;
|
||||
border-top: solid 1px silver;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* dark theme */
|
||||
.dark .panel {
|
||||
border: solid 2px #333;
|
||||
background: black;
|
||||
color: silver;
|
||||
box-shadow: 3px 3px 30px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.dark .panel summary {
|
||||
background: #333;
|
||||
}
|
||||
.dark .panel summary::-webkit-details-marker,
|
||||
.dark .sub-panel summary::-webkit-details-marker {
|
||||
color: #555;
|
||||
}
|
||||
.dark .sub-panel button,
|
||||
.dark .sub-panel .state,
|
||||
.dark .sub-panel {
|
||||
border: solid 1px #333;
|
||||
/* needed for dragging */
|
||||
background: #080808;
|
||||
color: #888;
|
||||
}
|
||||
.dark .sub-panel {
|
||||
border: solid 1px #333;
|
||||
}
|
||||
.dark .sub-panel.blink {
|
||||
box-shadow: 0px 0px 10px 0px rgba(255,255,0,1)
|
||||
}
|
||||
.dark .sub-panel summary {
|
||||
background: #333;
|
||||
color: silver;
|
||||
}
|
||||
.dark .sub-panel .state:active,
|
||||
.dark .sub-panel button:active {
|
||||
background: #222;
|
||||
}
|
||||
.dark .sub-panel .control .slider {
|
||||
border: solid 1px #555;
|
||||
background: black;
|
||||
}
|
||||
.dark .sub-panel .control.at-default .slider {
|
||||
}
|
||||
.dark .sub-panel .control .slider::-webkit-slider-thumb {
|
||||
border: solid 2px #aaa;
|
||||
background: black;
|
||||
box-shadow: 1px 1px 10px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.dark .sub-panel .control.at-default .slider::-webkit-slider-thumb {
|
||||
border: solid 1px gray;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.dark .sub-panel .control .value {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: gray;
|
||||
}
|
||||
.dark .sub-panel .control .reset:hover {
|
||||
border: solid 1px #333;
|
||||
}
|
||||
.dark .sub-panel hr {
|
||||
border: none;
|
||||
border-top: solid 1px #333;
|
||||
}
|
||||
.dark .side-panel {
|
||||
background: black;
|
||||
box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.dark .side-panel:not(:empty):hover:after {
|
||||
color: gray;
|
||||
}
|
||||
.dark .side-panel.left {
|
||||
border-right: solid 1px #333;
|
||||
}
|
||||
.dark .side-panel.right {
|
||||
border-left: solid 1px #333;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* gray theme */
|
||||
|
||||
.gray .panel {
|
||||
border: solid 2px #444;
|
||||
background: #333;
|
||||
color: silver;
|
||||
box-shadow: 3px 3px 30px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.gray .panel summary {
|
||||
background: #444;
|
||||
}
|
||||
.gray .panel summary::-webkit-details-marker,
|
||||
.gray .sub-panel summary::-webkit-details-marker {
|
||||
color: #555;
|
||||
}
|
||||
.gray .sub-panel button,
|
||||
.gray .sub-panel .state,
|
||||
.gray .sub-panel {
|
||||
border: solid 1px #444;
|
||||
/* needed for dragging */
|
||||
background: #333;
|
||||
color: #888;
|
||||
}
|
||||
.gray .sub-panel {
|
||||
border: solid 1px #454545;
|
||||
}
|
||||
.gray .sub-panel.blink {
|
||||
box-shadow: 0px 0px 10px 0px rgba(255,255,0,1)
|
||||
}
|
||||
.gray .sub-panel summary {
|
||||
background: #444;
|
||||
color: silver;
|
||||
}
|
||||
.gray .sub-panel .state:active,
|
||||
.gray .sub-panel button:active {
|
||||
background: #444;
|
||||
}
|
||||
.gray .sub-panel .control .slider {
|
||||
border: solid 1px #555;
|
||||
background: #222;
|
||||
}
|
||||
.gray .sub-panel .control.at-default .slider {
|
||||
}
|
||||
.gray .sub-panel .control .slider::-webkit-slider-thumb {
|
||||
border: solid 2px #aaa;
|
||||
background: #333;
|
||||
}
|
||||
.gray .sub-panel .control.at-default .slider::-webkit-slider-thumb {
|
||||
border: solid 1px gray;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.gray .sub-panel .control .value {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: gray;
|
||||
}
|
||||
.gray .sub-panel .control .reset:hover {
|
||||
border: solid 1px #444;
|
||||
}
|
||||
.gray .sub-panel hr {
|
||||
border: none;
|
||||
border-top: solid 1px #444;
|
||||
}
|
||||
.gray .side-panel {
|
||||
background: #303030;
|
||||
box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.gray .side-panel:not(:empty):hover:after {
|
||||
color: silver;
|
||||
}
|
||||
.gray .side-panel.left {
|
||||
border-right: solid 1px #444;
|
||||
}
|
||||
.gray .side-panel.right {
|
||||
border-left: solid 1px #444;
|
||||
}
|
||||
|
||||
1483
ui (gen4)/data.js
Executable file
1483
ui (gen4)/data.js
Executable file
File diff suppressed because it is too large
Load Diff
4
ui (gen4)/ext-lib/jquery-1.7.2.min.js
vendored
Executable file
4
ui (gen4)/ext-lib/jquery-1.7.2.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
9597
ui (gen4)/ext-lib/jquery-1.9.1.js
vendored
Executable file
9597
ui (gen4)/ext-lib/jquery-1.9.1.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
6
ui (gen4)/ext-lib/jquery-ui.js
vendored
Executable file
6
ui (gen4)/ext-lib/jquery-ui.js
vendored
Executable file
File diff suppressed because one or more lines are too long
4
ui (gen4)/ext-lib/jquery.js
vendored
Executable file
4
ui (gen4)/ext-lib/jquery.js
vendored
Executable file
File diff suppressed because one or more lines are too long
770
ui (gen4)/ext-lib/jstorage.js
Executable file
770
ui (gen4)/ext-lib/jstorage.js
Executable file
@ -0,0 +1,770 @@
|
||||
/*
|
||||
* ----------------------------- JSTORAGE -------------------------------------
|
||||
* Simple local storage wrapper to save data on the browser side, supporting
|
||||
* all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
|
||||
*
|
||||
* Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
|
||||
* Project homepage: www.jstorage.info
|
||||
*
|
||||
* Licensed under MIT-style license:
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
(function(){
|
||||
var
|
||||
/* jStorage version */
|
||||
JSTORAGE_VERSION = "0.2.3",
|
||||
|
||||
/* detect a dollar object or create one if not found */
|
||||
$ = window.jQuery || window.$ || (window.$ = {}),
|
||||
|
||||
/* check for a JSON handling support */
|
||||
JSON = {
|
||||
parse:
|
||||
window.JSON && (window.JSON.parse || window.JSON.decode) ||
|
||||
String.prototype.evalJSON && function(str){return String(str).evalJSON();} ||
|
||||
$.parseJSON ||
|
||||
$.evalJSON,
|
||||
stringify:
|
||||
window.JSON && (window.JSON.stringify || window.JSON.encode) ||
|
||||
Object.toJSON ||
|
||||
$.toJSON
|
||||
};
|
||||
|
||||
// Break if no JSON support was found
|
||||
if(!JSON.parse || !JSON.stringify){
|
||||
throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
|
||||
}
|
||||
|
||||
var
|
||||
/* This is the object, that holds the cached values */
|
||||
_storage = {},
|
||||
|
||||
/* Actual browser storage (localStorage or globalStorage['domain']) */
|
||||
_storage_service = {jStorage:"{}"},
|
||||
|
||||
/* DOM element for older IE versions, holds userData behavior */
|
||||
_storage_elm = null,
|
||||
|
||||
/* How much space does the storage take */
|
||||
_storage_size = 0,
|
||||
|
||||
/* which backend is currently used */
|
||||
_backend = false,
|
||||
|
||||
/* onchange observers */
|
||||
_observers = {},
|
||||
|
||||
/* timeout to wait after onchange event */
|
||||
_observerTimeout = false,
|
||||
|
||||
/* last update time */
|
||||
_observerUpdate = 0,
|
||||
|
||||
/* Next check for TTL */
|
||||
_ttl_timeout,
|
||||
|
||||
/* crc32 table */
|
||||
_crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+
|
||||
"0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+
|
||||
"6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+
|
||||
"FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+
|
||||
"A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+
|
||||
"32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+
|
||||
"56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+
|
||||
"C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+
|
||||
"E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+
|
||||
"6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+
|
||||
"12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+
|
||||
"A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+
|
||||
"DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+
|
||||
"5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+
|
||||
"2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+
|
||||
"04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+
|
||||
"7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+
|
||||
"FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+
|
||||
"A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+
|
||||
"36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+
|
||||
"5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+
|
||||
"C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+
|
||||
"EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+
|
||||
"7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+
|
||||
"18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+
|
||||
"A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+
|
||||
"D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+
|
||||
"53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+
|
||||
"2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D",
|
||||
|
||||
/**
|
||||
* XML encoding and decoding as XML nodes can't be JSON'ized
|
||||
* XML nodes are encoded and decoded if the node is the value to be saved
|
||||
* but not if it's as a property of another object
|
||||
* Eg. -
|
||||
* $.jStorage.set("key", xmlNode); // IS OK
|
||||
* $.jStorage.set("key", {xml: xmlNode}); // NOT OK
|
||||
*/
|
||||
_XMLService = {
|
||||
|
||||
/**
|
||||
* Validates a XML node to be XML
|
||||
* based on jQuery.isXML function
|
||||
*/
|
||||
isXML: function(elm){
|
||||
var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
|
||||
return documentElement ? documentElement.nodeName !== "HTML" : false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a XML node to string
|
||||
* based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
|
||||
*/
|
||||
encode: function(xmlNode) {
|
||||
if(!this.isXML(xmlNode)){
|
||||
return false;
|
||||
}
|
||||
try{ // Mozilla, Webkit, Opera
|
||||
return new XMLSerializer().serializeToString(xmlNode);
|
||||
}catch(E1) {
|
||||
try { // IE
|
||||
return xmlNode.xml;
|
||||
}catch(E2){}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decodes a XML node from string
|
||||
* loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
|
||||
*/
|
||||
decode: function(xmlString){
|
||||
var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
|
||||
(window.ActiveXObject && function(_xmlString) {
|
||||
var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
|
||||
xml_doc.async = 'false';
|
||||
xml_doc.loadXML(_xmlString);
|
||||
return xml_doc;
|
||||
}),
|
||||
resultXML;
|
||||
if(!dom_parser){
|
||||
return false;
|
||||
}
|
||||
resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
|
||||
return this.isXML(resultXML)?resultXML:false;
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////// PRIVATE METHODS ////////////////////////
|
||||
|
||||
/**
|
||||
* Initialization function. Detects if the browser supports DOM Storage
|
||||
* or userData behavior and behaves accordingly.
|
||||
*/
|
||||
function _init(){
|
||||
/* Check if browser supports localStorage */
|
||||
var localStorageReallyWorks = false;
|
||||
if("localStorage" in window){
|
||||
try {
|
||||
window.localStorage.setItem('_tmptest', 'tmpval');
|
||||
localStorageReallyWorks = true;
|
||||
window.localStorage.removeItem('_tmptest');
|
||||
} catch(BogusQuotaExceededErrorOnIos5) {
|
||||
// Thanks be to iOS5 Private Browsing mode which throws
|
||||
// QUOTA_EXCEEDED_ERRROR DOM Exception 22.
|
||||
}
|
||||
}
|
||||
if(localStorageReallyWorks){
|
||||
try {
|
||||
if(window.localStorage) {
|
||||
_storage_service = window.localStorage;
|
||||
_backend = "localStorage";
|
||||
_observerUpdate = _storage_service.jStorage_update;
|
||||
}
|
||||
} catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */}
|
||||
}
|
||||
/* Check if browser supports globalStorage */
|
||||
else if("globalStorage" in window){
|
||||
try {
|
||||
if(window.globalStorage) {
|
||||
_storage_service = window.globalStorage[window.location.hostname];
|
||||
_backend = "globalStorage";
|
||||
_observerUpdate = _storage_service.jStorage_update;
|
||||
}
|
||||
} catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */}
|
||||
}
|
||||
/* Check if browser supports userData behavior */
|
||||
else {
|
||||
_storage_elm = document.createElement('link');
|
||||
if(_storage_elm.addBehavior){
|
||||
|
||||
/* Use a DOM element to act as userData storage */
|
||||
_storage_elm.style.behavior = 'url(#default#userData)';
|
||||
|
||||
/* userData element needs to be inserted into the DOM! */
|
||||
document.getElementsByTagName('head')[0].appendChild(_storage_elm);
|
||||
|
||||
_storage_elm.load("jStorage");
|
||||
|
||||
var data = "{}";
|
||||
try{
|
||||
data = _storage_elm.getAttribute("jStorage");
|
||||
}catch(E5){}
|
||||
|
||||
try{
|
||||
_observerUpdate = _storage_elm.getAttribute("jStorage_update");
|
||||
}catch(E6){}
|
||||
|
||||
_storage_service.jStorage = data;
|
||||
_backend = "userDataBehavior";
|
||||
}else{
|
||||
_storage_elm = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_load_storage();
|
||||
|
||||
// remove dead keys
|
||||
_handleTTL();
|
||||
|
||||
// start listening for changes
|
||||
_setupObserver();
|
||||
}
|
||||
|
||||
function _reloadData(){
|
||||
var data = "{}";
|
||||
|
||||
if(_backend == "userDataBehavior"){
|
||||
_storage_elm.load("jStorage");
|
||||
|
||||
try{
|
||||
data = _storage_elm.getAttribute("jStorage");
|
||||
}catch(E5){}
|
||||
|
||||
try{
|
||||
_observerUpdate = _storage_elm.getAttribute("jStorage_update");
|
||||
}catch(E6){}
|
||||
|
||||
_storage_service.jStorage = data;
|
||||
}
|
||||
|
||||
_load_storage();
|
||||
|
||||
// remove dead keys
|
||||
_handleTTL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a storage change observer
|
||||
*/
|
||||
function _setupObserver(){
|
||||
if(_backend == "localStorage" || _backend == "globalStorage"){
|
||||
if("addEventListener" in window){
|
||||
window.addEventListener("storage", _storageObserver, false);
|
||||
}else{
|
||||
document.attachEvent("onstorage", _storageObserver);
|
||||
}
|
||||
}else if(_backend == "userDataBehavior"){
|
||||
setInterval(_storageObserver, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired on any kind of data change, needs to check if anything has
|
||||
* really been changed
|
||||
*/
|
||||
function _storageObserver(){
|
||||
var updateTime;
|
||||
// cumulate change notifications with timeout
|
||||
clearTimeout(_observerTimeout);
|
||||
_observerTimeout = setTimeout(function(){
|
||||
|
||||
if(_backend == "localStorage" || _backend == "globalStorage"){
|
||||
updateTime = _storage_service.jStorage_update;
|
||||
}else if(_backend == "userDataBehavior"){
|
||||
_storage_elm.load("jStorage");
|
||||
try{
|
||||
updateTime = _storage_elm.getAttribute("jStorage_update");
|
||||
}catch(E5){}
|
||||
}
|
||||
|
||||
if(updateTime && updateTime != _observerUpdate){
|
||||
_observerUpdate = updateTime;
|
||||
_checkUpdatedKeys();
|
||||
}
|
||||
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the data and checks if any keys are changed
|
||||
*/
|
||||
function _checkUpdatedKeys(){
|
||||
var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
|
||||
newCrc32List;
|
||||
|
||||
_reloadData();
|
||||
newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
|
||||
|
||||
var key,
|
||||
updated = [],
|
||||
removed = [];
|
||||
|
||||
for(key in oldCrc32List){
|
||||
if(oldCrc32List.hasOwnProperty(key)){
|
||||
if(!newCrc32List[key]){
|
||||
removed.push(key);
|
||||
continue;
|
||||
}
|
||||
if(oldCrc32List[key] != newCrc32List[key]){
|
||||
updated.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(key in newCrc32List){
|
||||
if(newCrc32List.hasOwnProperty(key)){
|
||||
if(!oldCrc32List[key]){
|
||||
updated.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fireObservers(updated, "updated");
|
||||
_fireObservers(removed, "deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires observers for updated keys
|
||||
*
|
||||
* @param {Array|String} keys Array of key names or a key
|
||||
* @param {String} action What happened with the value (updated, deleted, flushed)
|
||||
*/
|
||||
function _fireObservers(keys, action){
|
||||
keys = [].concat(keys || []);
|
||||
if(action == "flushed"){
|
||||
keys = [];
|
||||
for(var key in _observers){
|
||||
if(_observers.hasOwnProperty(key)){
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
action = "deleted";
|
||||
}
|
||||
for(var i=0, len = keys.length; i<len; i++){
|
||||
if(_observers[keys[i]]){
|
||||
for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
|
||||
_observers[keys[i]][j](keys[i], action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes key change to listeners
|
||||
*/
|
||||
function _publishChange(){
|
||||
var updateTime = (+new Date()).toString();
|
||||
|
||||
if(_backend == "localStorage" || _backend == "globalStorage"){
|
||||
_storage_service.jStorage_update = updateTime;
|
||||
}else if(_backend == "userDataBehavior"){
|
||||
_storage_elm.setAttribute("jStorage_update", updateTime);
|
||||
_storage_elm.save("jStorage");
|
||||
}
|
||||
|
||||
_storageObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the data from the storage based on the supported mechanism
|
||||
*/
|
||||
function _load_storage(){
|
||||
/* if jStorage string is retrieved, then decode it */
|
||||
if(_storage_service.jStorage){
|
||||
try{
|
||||
_storage = JSON.parse(String(_storage_service.jStorage));
|
||||
}catch(E6){_storage_service.jStorage = "{}";}
|
||||
}else{
|
||||
_storage_service.jStorage = "{}";
|
||||
}
|
||||
_storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
|
||||
|
||||
if(!_storage.__jstorage_meta){
|
||||
_storage.__jstorage_meta = {};
|
||||
}
|
||||
if(!_storage.__jstorage_meta.CRC32){
|
||||
_storage.__jstorage_meta.CRC32 = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions provides the "save" mechanism to store the jStorage object
|
||||
*/
|
||||
function _save(){
|
||||
try{
|
||||
_storage_service.jStorage = JSON.stringify(_storage);
|
||||
// If userData is used as the storage engine, additional
|
||||
if(_storage_elm) {
|
||||
_storage_elm.setAttribute("jStorage",_storage_service.jStorage);
|
||||
_storage_elm.save("jStorage");
|
||||
}
|
||||
_storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
|
||||
}catch(E7){/* probably cache is full, nothing is saved this way*/}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function checks if a key is set and is string or numberic
|
||||
*
|
||||
* @param {String} key Key name
|
||||
*/
|
||||
function _checkKey(key){
|
||||
if(!key || (typeof key != "string" && typeof key != "number")){
|
||||
throw new TypeError('Key name must be string or numeric');
|
||||
}
|
||||
if(key == "__jstorage_meta"){
|
||||
throw new TypeError('Reserved key name');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes expired keys
|
||||
*/
|
||||
function _handleTTL(){
|
||||
var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
|
||||
|
||||
clearTimeout(_ttl_timeout);
|
||||
|
||||
if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
curtime = +new Date();
|
||||
TTL = _storage.__jstorage_meta.TTL;
|
||||
|
||||
CRC32 = _storage.__jstorage_meta.CRC32;
|
||||
for(i in TTL){
|
||||
if(TTL.hasOwnProperty(i)){
|
||||
if(TTL[i] <= curtime){
|
||||
delete TTL[i];
|
||||
delete CRC32[i];
|
||||
delete _storage[i];
|
||||
changed = true;
|
||||
deleted.push(i);
|
||||
}else if(TTL[i] < nextExpire){
|
||||
nextExpire = TTL[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set next check
|
||||
if(nextExpire != Infinity){
|
||||
_ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
|
||||
}
|
||||
|
||||
// save changes
|
||||
if(changed){
|
||||
_save();
|
||||
_publishChange();
|
||||
_fireObservers(deleted, "deleted");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
|
||||
*
|
||||
* @param {String} str String to be hashed
|
||||
* @param {Number} [crc] Last crc value in case of streams
|
||||
*/
|
||||
function _crc32(str, crc){
|
||||
crc = crc || 0;
|
||||
|
||||
var n = 0, //a number between 0 and 255
|
||||
x = 0; //an hex number
|
||||
|
||||
crc = crc ^ (-1);
|
||||
for(var i = 0, len = str.length; i < len; i++){
|
||||
n = (crc ^ str.charCodeAt(i)) & 0xFF;
|
||||
x = "0x" + _crc32Table.substr(n * 9, 8);
|
||||
crc = (crc >>> 8)^x;
|
||||
}
|
||||
return crc^(-1);
|
||||
}
|
||||
|
||||
////////////////////////// PUBLIC INTERFACE /////////////////////////
|
||||
|
||||
$.jStorage = {
|
||||
/* Version number */
|
||||
version: JSTORAGE_VERSION,
|
||||
|
||||
/**
|
||||
* Sets a key's value.
|
||||
*
|
||||
* @param {String} key Key to set. If this value is not set or not
|
||||
* a string an exception is raised.
|
||||
* @param {Mixed} value Value to set. This can be any value that is JSON
|
||||
* compatible (Numbers, Strings, Objects etc.).
|
||||
* @param {Object} [options] - possible options to use
|
||||
* @param {Number} [options.TTL] - optional TTL value
|
||||
* @return {Mixed} the used value
|
||||
*/
|
||||
set: function(key, value, options){
|
||||
_checkKey(key);
|
||||
|
||||
options = options || {};
|
||||
|
||||
// undefined values are deleted automatically
|
||||
if(typeof value == "undefined"){
|
||||
this.deleteKey(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
if(_XMLService.isXML(value)){
|
||||
value = {_is_xml:true,xml:_XMLService.encode(value)};
|
||||
}else if(typeof value == "function"){
|
||||
return undefined; // functions can't be saved!
|
||||
}else if(value && typeof value == "object"){
|
||||
// clone the object before saving to _storage tree
|
||||
value = JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
_storage[key] = value;
|
||||
|
||||
_storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value));
|
||||
|
||||
this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
|
||||
|
||||
_fireObservers(key, "updated");
|
||||
return value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Looks up a key in cache
|
||||
*
|
||||
* @param {String} key - Key to look up.
|
||||
* @param {mixed} def - Default value to return, if key didn't exist.
|
||||
* @return {Mixed} the key value, default value or null
|
||||
*/
|
||||
get: function(key, def){
|
||||
_checkKey(key);
|
||||
if(key in _storage){
|
||||
if(_storage[key] && typeof _storage[key] == "object" &&
|
||||
_storage[key]._is_xml &&
|
||||
_storage[key]._is_xml){
|
||||
return _XMLService.decode(_storage[key].xml);
|
||||
}else{
|
||||
return _storage[key];
|
||||
}
|
||||
}
|
||||
return typeof(def) == 'undefined' ? null : def;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a key from cache.
|
||||
*
|
||||
* @param {String} key - Key to delete.
|
||||
* @return {Boolean} true if key existed or false if it didn't
|
||||
*/
|
||||
deleteKey: function(key){
|
||||
_checkKey(key);
|
||||
if(key in _storage){
|
||||
delete _storage[key];
|
||||
// remove from TTL list
|
||||
if(typeof _storage.__jstorage_meta.TTL == "object" &&
|
||||
key in _storage.__jstorage_meta.TTL){
|
||||
delete _storage.__jstorage_meta.TTL[key];
|
||||
}
|
||||
|
||||
delete _storage.__jstorage_meta.CRC32[key];
|
||||
|
||||
_save();
|
||||
_publishChange();
|
||||
_fireObservers(key, "deleted");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a TTL for a key, or remove it if ttl value is 0 or below
|
||||
*
|
||||
* @param {String} key - key to set the TTL for
|
||||
* @param {Number} ttl - TTL timeout in milliseconds
|
||||
* @return {Boolean} true if key existed or false if it didn't
|
||||
*/
|
||||
setTTL: function(key, ttl){
|
||||
var curtime = +new Date();
|
||||
_checkKey(key);
|
||||
ttl = Number(ttl) || 0;
|
||||
if(key in _storage){
|
||||
|
||||
if(!_storage.__jstorage_meta.TTL){
|
||||
_storage.__jstorage_meta.TTL = {};
|
||||
}
|
||||
|
||||
// Set TTL value for the key
|
||||
if(ttl>0){
|
||||
_storage.__jstorage_meta.TTL[key] = curtime + ttl;
|
||||
}else{
|
||||
delete _storage.__jstorage_meta.TTL[key];
|
||||
}
|
||||
|
||||
_save();
|
||||
|
||||
_handleTTL();
|
||||
|
||||
_publishChange();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
|
||||
*
|
||||
* @param {String} key Key to check
|
||||
* @return {Number} Remaining TTL in milliseconds
|
||||
*/
|
||||
getTTL: function(key){
|
||||
var curtime = +new Date(), ttl;
|
||||
_checkKey(key);
|
||||
if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){
|
||||
ttl = _storage.__jstorage_meta.TTL[key] - curtime;
|
||||
return ttl || 0;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes everything in cache.
|
||||
*
|
||||
* @return {Boolean} Always true
|
||||
*/
|
||||
flush: function(){
|
||||
_storage = {__jstorage_meta:{CRC32:{}}};
|
||||
_save();
|
||||
_publishChange();
|
||||
_fireObservers(null, "flushed");
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a read-only copy of _storage
|
||||
*
|
||||
* @return {Object} Read-only copy of _storage
|
||||
*/
|
||||
storageObj: function(){
|
||||
function F() {}
|
||||
F.prototype = _storage;
|
||||
return new F();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an index of all used keys as an array
|
||||
* ['key1', 'key2',..'keyN']
|
||||
*
|
||||
* @return {Array} Used keys
|
||||
*/
|
||||
index: function(){
|
||||
var index = [], i;
|
||||
for(i in _storage){
|
||||
if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
|
||||
index.push(i);
|
||||
}
|
||||
}
|
||||
return index;
|
||||
},
|
||||
|
||||
/**
|
||||
* How much space in bytes does the storage take?
|
||||
*
|
||||
* @return {Number} Storage size in chars (not the same as in bytes,
|
||||
* since some chars may take several bytes)
|
||||
*/
|
||||
storageSize: function(){
|
||||
return _storage_size;
|
||||
},
|
||||
|
||||
/**
|
||||
* Which backend is currently in use?
|
||||
*
|
||||
* @return {String} Backend name
|
||||
*/
|
||||
currentBackend: function(){
|
||||
return _backend;
|
||||
},
|
||||
|
||||
/**
|
||||
* Test if storage is available
|
||||
*
|
||||
* @return {Boolean} True if storage can be used
|
||||
*/
|
||||
storageAvailable: function(){
|
||||
return !!_backend;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register change listeners
|
||||
*
|
||||
* @param {String} key Key name
|
||||
* @param {Function} callback Function to run when the key changes
|
||||
*/
|
||||
listenKeyChange: function(key, callback){
|
||||
_checkKey(key);
|
||||
if(!_observers[key]){
|
||||
_observers[key] = [];
|
||||
}
|
||||
_observers[key].push(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove change listeners
|
||||
*
|
||||
* @param {String} key Key name to unregister listeners against
|
||||
* @param {Function} [callback] If set, unregister the callback, if not - unregister all
|
||||
*/
|
||||
stopListening: function(key, callback){
|
||||
_checkKey(key);
|
||||
|
||||
if(!_observers[key]){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!callback){
|
||||
delete _observers[key];
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = _observers[key].length - 1; i>=0; i--){
|
||||
if(_observers[key][i] == callback){
|
||||
_observers[key].splice(i,1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reloads the data from browser storage
|
||||
*/
|
||||
reInit: function(){
|
||||
_reloadData();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize jStorage
|
||||
_init();
|
||||
|
||||
})();
|
||||
9
ui (gen4)/ext-lib/less-1.3.3.min.js
vendored
Executable file
9
ui (gen4)/ext-lib/less-1.3.3.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
9
ui (gen4)/ext-lib/less.js
Executable file
9
ui (gen4)/ext-lib/less.js
Executable file
File diff suppressed because one or more lines are too long
349
ui (gen4)/image.js
Executable file
349
ui (gen4)/image.js
Executable file
@ -0,0 +1,349 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
// A stub image, also here for documentation...
|
||||
var STUB_IMAGE_DATA = {
|
||||
// Entity GID...
|
||||
id: 'STUB-GID',
|
||||
|
||||
// Entity type
|
||||
//
|
||||
// can be:
|
||||
// - 'image'
|
||||
// - 'group'
|
||||
type: 'image',
|
||||
|
||||
// Entity state
|
||||
//
|
||||
// can be:
|
||||
// - 'single'
|
||||
// - 'grouped'
|
||||
// - 'hidden'
|
||||
// - ...
|
||||
state: 'single',
|
||||
|
||||
// Creation time...
|
||||
ctime: 0,
|
||||
|
||||
// Original path...
|
||||
path: './images/sizes/900px/SIZE.jpg',
|
||||
|
||||
// Previews...
|
||||
// NOTE: the actual values depend on specific image and can be
|
||||
// any size...
|
||||
preview: {
|
||||
'150px': './images/sizes/150px/SIZE.jpg',
|
||||
'350px': './images/sizes/350px/SIZE.jpg',
|
||||
'900px': './images/sizes/900px/SIZE.jpg',
|
||||
},
|
||||
|
||||
// Classes
|
||||
// XXX currently unused...
|
||||
classes: '',
|
||||
|
||||
// Image orientation (optional)
|
||||
//
|
||||
// can be:
|
||||
// - null/undefined - same as 0
|
||||
// - 0 (default) - load as-is
|
||||
// - 90 - rotate 90deg CW
|
||||
// - 180 - rotate 180deg CW
|
||||
// - 270 - rotate 270deg CW (90deg CCW)
|
||||
//
|
||||
// NOTE: use orientationExif2ImageGrid(..) to convert from EXIF
|
||||
// orientation format to ImageGrid format...
|
||||
orientation: 0,
|
||||
|
||||
// Image flip state (optional)
|
||||
//
|
||||
// can be:
|
||||
// - null/undefined
|
||||
// - array
|
||||
//
|
||||
// can contain:
|
||||
// - 'vertical'
|
||||
// - 'horizontal'
|
||||
//
|
||||
// NOTE: use orientationExif2ImageGrid(..) to convert from EXIF
|
||||
// orientation format to ImageGrid format...
|
||||
flipped: null,
|
||||
|
||||
// Image comment (optional)
|
||||
//
|
||||
// can be:
|
||||
// - null/undefined
|
||||
// - string
|
||||
comment: null,
|
||||
|
||||
// List of image tags (optional)
|
||||
//
|
||||
// can be:
|
||||
// - null/undefined
|
||||
// - array
|
||||
tags: null,
|
||||
}
|
||||
|
||||
|
||||
// List of function that update image state...
|
||||
//
|
||||
// these are called by updateImage(..) after the image is created.
|
||||
//
|
||||
// each function must be of the form:
|
||||
// updateImage(gid, image) -> image
|
||||
//
|
||||
var IMAGE_UPDATERS = []
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// XXX Constructors...
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Run all the image update functions registered in IMAGE_UPDATERS, on
|
||||
// an image...
|
||||
//
|
||||
function updateImageIndicators(gid, image){
|
||||
gid = gid == null ? getImageGID() : gid
|
||||
image = image == null ? getImage() : $(image)
|
||||
|
||||
IMAGE_UPDATERS.forEach(function(update){
|
||||
update(gid, image)
|
||||
})
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
|
||||
// helper...
|
||||
function _loadImagePreviewURL(image, url){
|
||||
// pre-cache and load image...
|
||||
// NOTE: this will make images load without a blackout...
|
||||
var img = new Image()
|
||||
img.onload = function(){
|
||||
image.css({
|
||||
'background-image': 'url("'+ url +'")',
|
||||
})
|
||||
}
|
||||
img.src = url
|
||||
return img
|
||||
}
|
||||
|
||||
|
||||
// Update an image element
|
||||
//
|
||||
// NOTE: care must be taken to reset ALL attributes an image can have,
|
||||
// a common bug if this is not done correctly, is that some settings
|
||||
// may leak to newly loaded images...
|
||||
function updateImage(image, gid, size, sync){
|
||||
image = image == null ? getImage() : $(image)
|
||||
sync = sync == null ? CONFIG.load_img_sync : sync
|
||||
var old_gid = getImageGID(image)
|
||||
|
||||
// same image -- update...
|
||||
if(old_gid == gid || gid == null){
|
||||
gid = old_gid
|
||||
|
||||
// reuse for different image -- reconstruct...
|
||||
} else {
|
||||
// remove old marks...
|
||||
if(typeof(old_gid) == typeof('str')){
|
||||
getImageMarks(old_gid).remove()
|
||||
}
|
||||
// reset gid...
|
||||
image
|
||||
.attr('gid', JSON.stringify(gid))
|
||||
.css({
|
||||
// clear the old preview...
|
||||
'background-image': '',
|
||||
})
|
||||
}
|
||||
size = size == null ? getVisibleImageSize('max') : size
|
||||
|
||||
// get the image data...
|
||||
var img_data = IMAGES[gid]
|
||||
if(img_data == null){
|
||||
img_data = STUB_IMAGE_DATA
|
||||
}
|
||||
|
||||
/* XXX does not seem to be needing this...
|
||||
// set the current class...
|
||||
if(gid == DATA.current){
|
||||
image.addClass('current')
|
||||
} else {
|
||||
image.removeClass('current')
|
||||
}
|
||||
*/
|
||||
|
||||
// preview...
|
||||
var p_url = getBestPreview(gid, size).url
|
||||
|
||||
// update the preview if it's a new image or...
|
||||
if(old_gid != gid
|
||||
// the new preview (purl) is different to current...
|
||||
|| image.css('background-image').indexOf(encodeURI(p_url)) < 0){
|
||||
// sync load...
|
||||
if(sync){
|
||||
_loadImagePreviewURL(image, p_url)
|
||||
|
||||
// async load...
|
||||
} else {
|
||||
// NOTE: storing the url in .data() makes the image load the
|
||||
// last requested preview and in a case when we manage to
|
||||
// call updateImage(...) on the same element multiple times
|
||||
// before the previews get loaded...
|
||||
// ...setting the data().loading is sync while loading an
|
||||
// image is not, and if several loads are done in sequence
|
||||
// there is no guarantee that they will happen in the same
|
||||
// order as requested...
|
||||
image.data().loading = p_url
|
||||
setTimeout(function(){
|
||||
_loadImagePreviewURL(image, image.data().loading)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// main attrs...
|
||||
image
|
||||
.attr({
|
||||
order: DATA.order.indexOf(gid),
|
||||
orientation: img_data.orientation == null ? 0 : img_data.orientation,
|
||||
})
|
||||
|
||||
// flip...
|
||||
setImageFlipState(image, img_data.flipped == null ? [] : img_data.flipped)
|
||||
|
||||
// NOTE: this only has effect on non-square image blocks...
|
||||
correctImageProportionsForRotation(image)
|
||||
|
||||
// marks and other indicators...
|
||||
updateImageIndicators(gid, image)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
|
||||
// Same as updateImage(...) but will update all loaded images.
|
||||
//
|
||||
// If list is passed this will update only the images in the list. The
|
||||
// list can contain either gids or image elements.
|
||||
//
|
||||
// If CONFIG.update_sort_enabled is set, this will prioritize images by
|
||||
// distance from current image, loading the closest images first...
|
||||
//
|
||||
// If CONFIG.update_sync is set, this will run asynchronously.
|
||||
function updateImages(list, size, cmp){
|
||||
var deferred = $.Deferred()
|
||||
|
||||
function _worker(){
|
||||
list = list == null ? $('.image') : $(list)
|
||||
size = size == null ? getVisibleImageSize('max') : size
|
||||
|
||||
function _update(_, e){
|
||||
var img = typeof(e) == typeof('str') ? getImage(e) : $(e)
|
||||
if(img.length > 0){
|
||||
updateImage(img, null, size)
|
||||
}
|
||||
}
|
||||
|
||||
// sorted run...
|
||||
if(CONFIG.update_sort_enabled && cmp != false){
|
||||
cmp = cmp == null ?
|
||||
makeGIDDistanceCmp(getImageGID(), function(e){
|
||||
return typeof(e) == typeof('str') ? e : getImageGID(e)
|
||||
})
|
||||
// XXX this is more correct but is slow...
|
||||
//makeGIDRibbonDistanceCmp(getImageGID(), getImageGID)
|
||||
: cmp
|
||||
deferred.resolve(list
|
||||
// sort images by distance from current, so as to update what
|
||||
// the user is looking at first...
|
||||
.sort(cmp)
|
||||
.map(_update))
|
||||
|
||||
// do a fast run w.o. sorting images...
|
||||
} else {
|
||||
deferred.resolve(list.map(_update))
|
||||
}
|
||||
}
|
||||
|
||||
if(CONFIG.update_sync){
|
||||
_worker()
|
||||
} else {
|
||||
setTimeout(_worker, 0)
|
||||
}
|
||||
|
||||
return deferred
|
||||
}
|
||||
|
||||
|
||||
// Compensate for viewer proportioned and rotated images.
|
||||
//
|
||||
// This will set the margins so as to make the rotated image offset the
|
||||
// same space as it is occupying visually...
|
||||
//
|
||||
// NOTE: this is not needed for square image blocks.
|
||||
// NOTE: if an image block is square, this will remove the margins.
|
||||
function correctImageProportionsForRotation(images, container){
|
||||
container = container == null ? $('.viewer') : container
|
||||
|
||||
var W = container.innerWidth()
|
||||
var H = container.innerHeight()
|
||||
|
||||
var viewer_p = W > H ? 'landscape' : 'portrait'
|
||||
|
||||
return $(images).each(function(i, e){
|
||||
var image = $(this)
|
||||
// orientation...
|
||||
var o = image.attr('orientation')
|
||||
o = o == null ? 0 : o
|
||||
var w = image.outerWidth()
|
||||
var h = image.outerHeight()
|
||||
|
||||
// non-square image...
|
||||
if(w != h){
|
||||
|
||||
var image_p = w > h ? 'landscape' : 'portrait'
|
||||
|
||||
// when the image is turned 90deg/270deg and its
|
||||
// proportions are the same as the screen...
|
||||
if((o == 90 || o == 270) && image_p == viewer_p){
|
||||
image.css({
|
||||
width: h,
|
||||
height: w,
|
||||
})
|
||||
image.css({
|
||||
'margin-top': -((w - h)/2),
|
||||
'margin-bottom': -((w - h)/2),
|
||||
'margin-left': (w - h)/2,
|
||||
'margin-right': (w - h)/2,
|
||||
})
|
||||
|
||||
} else if((o == 0 || o == 180) && image_p != viewer_p){
|
||||
image.css({
|
||||
width: h,
|
||||
height: w,
|
||||
})
|
||||
image.css({
|
||||
'margin': '',
|
||||
})
|
||||
}
|
||||
|
||||
// square image...
|
||||
} else {
|
||||
image.css({
|
||||
'margin': '',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
56
ui (gen4)/index.html
Executable file
56
ui (gen4)/index.html
Executable file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- This is the basic viewer structure...
|
||||
|
||||
Unpopulated
|
||||
NOTE: there can be only .ribbon-set element.
|
||||
|
||||
<div class="viewer">
|
||||
<div class="ribbon-set"></div>
|
||||
</div>
|
||||
|
||||
|
||||
Populated
|
||||
|
||||
<div class="viewer">
|
||||
<div class="ribbon-set">
|
||||
<div class="ribbon">
|
||||
<div class="image"></div>
|
||||
<div class="image"></div>
|
||||
...
|
||||
</div>
|
||||
<div class="ribbon">
|
||||
<div class="image"></div>
|
||||
<div class="current image"></div>
|
||||
<div class="image"></div>
|
||||
<div class="mark selected"></div>
|
||||
<div class="image"></div>
|
||||
...
|
||||
</div>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="viewer">
|
||||
|
||||
<div class="ribbon-set"></div>
|
||||
|
||||
|
||||
<!-- XXX should these be here??? -->
|
||||
<div class="overlay-block">
|
||||
<div class="background"></div>
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- vim:set ts=4 sw=4 spell : -->
|
||||
</body>
|
||||
</html>
|
||||
70
ui (gen4)/interaction.js
Executable file
70
ui (gen4)/interaction.js
Executable file
@ -0,0 +1,70 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
//
|
||||
// Basic terms:
|
||||
// - trigger
|
||||
// this is similar to an event bind...
|
||||
// - filter
|
||||
// - action
|
||||
// fast reaction to instantanious actions, this is the same as an
|
||||
// event handler...
|
||||
// - feedback
|
||||
// feedback loop used for long interactions
|
||||
//
|
||||
// * might be a good idea to combine trigger and filter...
|
||||
//
|
||||
//
|
||||
// DSL loading stages:
|
||||
// Stage 1: Read.
|
||||
// - read the code
|
||||
// - eval the code
|
||||
// - introspection
|
||||
// Stage 2: Run.
|
||||
// - install hooks
|
||||
// - introspection
|
||||
// - run the handlers
|
||||
//
|
||||
//
|
||||
/*********************************************************************/
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
// Slang version candidate:
|
||||
//
|
||||
// on click
|
||||
// if [ ... ]
|
||||
// do [ ... ]
|
||||
//
|
||||
// if [ ... ]
|
||||
// key X
|
||||
// do [ ... ]
|
||||
//
|
||||
|
||||
var context = Context('test')
|
||||
// trigger...
|
||||
.on('click')
|
||||
// filter...
|
||||
.when(function(){ return true })
|
||||
// action...
|
||||
.act(function(){
|
||||
return
|
||||
})
|
||||
// action...
|
||||
.done()
|
||||
.when(function(){ return true })
|
||||
.key('X')
|
||||
.act(function(){ })
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
16
ui (gen4)/lib/_template.js
Executable file
16
ui (gen4)/lib/_template.js
Executable file
@ -0,0 +1,16 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
632
ui (gen4)/lib/dialogs.js
Executable file
632
ui (gen4)/lib/dialogs.js
Executable file
@ -0,0 +1,632 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Modal dialogs...
|
||||
*/
|
||||
|
||||
/********************************************************* Helpers ***/
|
||||
|
||||
// Set element text and tooltip
|
||||
//
|
||||
// NOTE: when text is a list, we will only use the first and the last
|
||||
// elements...
|
||||
// NOTE: if tip_elem is not given then both the text and tip will be set
|
||||
// on text_elem
|
||||
//
|
||||
// XXX add support for quoted '|'...
|
||||
function setTextWithTooltip(text, text_elem, tip_elem){
|
||||
text_elem = $(text_elem)
|
||||
tip_elem = tip_elem == null ? text_elem : tip_elem
|
||||
|
||||
if(typeof(text) != typeof('str')){
|
||||
tip = text
|
||||
} else {
|
||||
var tip = text.split(/\s*\|\s*/)
|
||||
}
|
||||
|
||||
// set elemnt text...
|
||||
text_elem
|
||||
.html(tip[0])
|
||||
|
||||
// do the tooltip...
|
||||
tip = tip.slice(1)
|
||||
tip = tip[tip.length-1]
|
||||
if(tip != null && tip.trim().length > 0){
|
||||
$('<span class="tooltip-icon tooltip-right"> *</span>')
|
||||
.attr('tooltip', tip)
|
||||
.appendTo(tip_elem)
|
||||
}
|
||||
|
||||
return text_elem
|
||||
}
|
||||
|
||||
|
||||
function getOverlay(root){
|
||||
root = $(root)
|
||||
var overlay = root.find('.overlay-block')
|
||||
if(overlay.length == 0){
|
||||
return $('<div class="overlay-block">'+
|
||||
'<div class="background"/>'+
|
||||
'<div class="content"/>'+
|
||||
'</div>').appendTo(root)
|
||||
}
|
||||
return overlay
|
||||
}
|
||||
|
||||
|
||||
function showInOverlay(root, data){
|
||||
root = $(root)
|
||||
|
||||
var overlay = getOverlay(root)
|
||||
|
||||
|
||||
if(data != null){
|
||||
var container = $('<table width="100%" height="100%"><tr><td align="center" valign="center">'+
|
||||
'<div class="dialog"/>'+
|
||||
'</td></tr></table>')
|
||||
var dialog = container.find('.dialog')
|
||||
|
||||
//overlay.find('.background')
|
||||
// .click(function(){ hideOverlay(root) })
|
||||
|
||||
dialog
|
||||
.append(data)
|
||||
.on('click', function(evt){
|
||||
evt.stopPropagation()
|
||||
})
|
||||
overlay.find('.content')
|
||||
.on('click', function(){
|
||||
overlay.trigger('close')
|
||||
hideOverlay(root)
|
||||
})
|
||||
.on('close accept', function(){
|
||||
//hideOverlay(root)
|
||||
})
|
||||
.append(container)
|
||||
}
|
||||
|
||||
root.addClass('overlay')
|
||||
|
||||
return overlay
|
||||
}
|
||||
|
||||
|
||||
function hideOverlay(root){
|
||||
root.removeClass('overlay')
|
||||
root.find('.overlay-block')
|
||||
.trigger('close')
|
||||
.remove()
|
||||
}
|
||||
|
||||
function isOverlayVisible(root){
|
||||
return getOverlay(root).css('display') != 'none'
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Field definitions...
|
||||
*/
|
||||
|
||||
var FIELD_TYPES = {
|
||||
// a simple hr...
|
||||
//
|
||||
// format:
|
||||
// '---'
|
||||
// Three or more '-'s
|
||||
hr: {
|
||||
type: 'hr',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<hr>',
|
||||
test: function(val){
|
||||
return /\-\-\-+/.test(val)
|
||||
},
|
||||
},
|
||||
// a simple br...
|
||||
//
|
||||
// format:
|
||||
// ' '
|
||||
// Three or more spaces
|
||||
br: {
|
||||
type: 'br',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<br>',
|
||||
test: function(val){
|
||||
return /\s\s\s+/.test(val)
|
||||
},
|
||||
},
|
||||
// format:
|
||||
// {
|
||||
// html: <html-block>
|
||||
// }
|
||||
html: {
|
||||
type: 'html',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="html-block"/>',
|
||||
test: function(val){
|
||||
return val.html != null
|
||||
},
|
||||
set: function(field, value){
|
||||
if(typeof(value.html) == typeof('str')){
|
||||
field.html(value.html)
|
||||
} else {
|
||||
field.append(value.html)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// format:
|
||||
// string
|
||||
// XXX add datalist option...
|
||||
// XXX make this textarea compatible...
|
||||
text: {
|
||||
type: 'text',
|
||||
text: null,
|
||||
default: '',
|
||||
html: '<div class="field string">'+
|
||||
'<span class="text"></span>'+
|
||||
'<input type="text" class="value">'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return typeof(val) == typeof('abc')
|
||||
},
|
||||
set: function(field, value){
|
||||
$(field).find('.value').attr('value', value)
|
||||
},
|
||||
get: function(field){
|
||||
return $(field).find('.value').attr('value')
|
||||
},
|
||||
},
|
||||
|
||||
// format:
|
||||
// true | false
|
||||
bool: {
|
||||
type: 'bool',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field checkbox">'+
|
||||
'<label><input type="checkbox" class="value">'+
|
||||
'<span class="text"></span></label>'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return val === true || val === false
|
||||
},
|
||||
set: function(field, value){
|
||||
if(value){
|
||||
$(field).find('.value').attr('checked', '')
|
||||
} else {
|
||||
$(field).find('.value').removeAttr('checked')
|
||||
}
|
||||
},
|
||||
get: function(field){
|
||||
return $(field).find('.value').attr('checked') == 'checked'
|
||||
},
|
||||
},
|
||||
|
||||
// NOTE: this will not work without node-webkit...
|
||||
// format:
|
||||
// { dir: <default-path> }
|
||||
dir: {
|
||||
type: 'dir',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field checkbox">'+
|
||||
'<span class="text"></span>'+
|
||||
'<input type="file" class="value" nwdirectory />'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return typeof(val) == typeof({}) && 'dir' in val
|
||||
},
|
||||
set: function(field, value){
|
||||
field.find('.value').attr('nwworkingdir', value.dir)
|
||||
},
|
||||
get: function(field){
|
||||
var f = $(field).find('.value')[0].files
|
||||
if(f.length == 0){
|
||||
return ''
|
||||
}
|
||||
return f[0].path
|
||||
},
|
||||
},
|
||||
|
||||
// NOTE: this will not work without node-webkit...
|
||||
// format:
|
||||
// { dir: <default-path> }
|
||||
// XXX add datalist option...
|
||||
ndir: {
|
||||
type: 'ndir',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field dir">'+
|
||||
'<span class="text"></span>'+
|
||||
'<input type="text" class="path"/>'+
|
||||
'<button class="browse">Browse</button>'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return typeof(val) == typeof({}) && 'ndir' in val
|
||||
},
|
||||
set: function(field, value){
|
||||
var that = this
|
||||
|
||||
// NOTE: we are attaching the file browser to body to avoid
|
||||
// click events on it closing the dialog...
|
||||
// ...for some reason stopPropagation(...) does not do
|
||||
// the job...
|
||||
var file = $('<input type="file" class="value" nwdirectory/>')
|
||||
.attr('nwworkingdir', value.ndir)
|
||||
.change(function(){
|
||||
var p = file[0].files
|
||||
if(p.length != 0){
|
||||
field.find('.path').val(p[0].path)
|
||||
}
|
||||
file.detach()
|
||||
// focus+select the path field...
|
||||
// NOTE: this is here to enable fast select-open
|
||||
// keyboard cycle (tab, enter, <select path>,
|
||||
// enter, enter)...
|
||||
field.find('.path')
|
||||
.focus()
|
||||
.select()
|
||||
})
|
||||
.hide()
|
||||
field.find('.path').val(value.ndir)
|
||||
|
||||
field.find('.browse').click(function(){
|
||||
file
|
||||
// load user input path...
|
||||
.attr('nwworkingdir', field.find('.path').val())
|
||||
.appendTo($('body'))
|
||||
.click()
|
||||
})
|
||||
|
||||
},
|
||||
get: function(field){
|
||||
return field.find('.path').val()
|
||||
},
|
||||
},
|
||||
|
||||
// format:
|
||||
// ['a', 'b', 'c', ...]
|
||||
//
|
||||
// an item can be of the folowing format:
|
||||
// <text> ['|' 'default' | 'disabled' ] [ '|' <tool-tip> ]
|
||||
//
|
||||
// NOTE: only one 'default' item should be present.
|
||||
// NOTE: if no defaults are set, then the first item is checked.
|
||||
choice: {
|
||||
type: 'choice',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field choice">'+
|
||||
'<span class="text"></span>'+
|
||||
'<div class="item"><label>'+
|
||||
'<input type="radio" class="value"/>'+
|
||||
'<span class="item-text"></span>'+
|
||||
'</label></div>'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return typeof(val) == typeof([]) && val.constructor.name == 'Array'
|
||||
},
|
||||
set: function(field, value){
|
||||
var t = field.find('.text').html()
|
||||
t = t == '' ? Math.random()+'' : t
|
||||
var item = field.find('.item').last()
|
||||
for(var i=0; i < value.length; i++){
|
||||
// get options...
|
||||
var opts = value[i]
|
||||
.split(/\|/g)
|
||||
.map(function(e){ return e.trim() })
|
||||
|
||||
var val = item.find('.value')
|
||||
val.val(opts[0])
|
||||
|
||||
// set checked state...
|
||||
if(opts.slice(1).indexOf('default') >= 0){
|
||||
val.prop('checked', true)
|
||||
opts.splice(opts.indexOf('default'), 1)
|
||||
} else {
|
||||
val.prop('checked', false)
|
||||
}
|
||||
|
||||
// set disabled state...
|
||||
if(opts.slice(1).indexOf('disabled') >= 0){
|
||||
val.prop('disabled', true)
|
||||
opts.splice(opts.indexOf('disabled'), 1)
|
||||
item.addClass('disabled')
|
||||
} else {
|
||||
val.prop('disabled', false)
|
||||
item.removeClass('disabled')
|
||||
}
|
||||
|
||||
setTextWithTooltip(opts, item.find('.item-text'))
|
||||
|
||||
item.appendTo(field)
|
||||
|
||||
item = item.clone()
|
||||
}
|
||||
var values = field.find('.value')
|
||||
.attr('name', t)
|
||||
// set the default...
|
||||
if(values.filter(':checked:not([disabled])').length == 0){
|
||||
values.filter(':not([disabled])').first()
|
||||
.prop('checked', true)
|
||||
}
|
||||
},
|
||||
get: function(field){
|
||||
return $(field).find('.value:checked').val()
|
||||
},
|
||||
},
|
||||
|
||||
// format:
|
||||
// {
|
||||
// select: ['a', 'b', 'c', ...]
|
||||
// // default option (optional)...
|
||||
// default: <number> | <text>
|
||||
// }
|
||||
select: {
|
||||
type: 'select',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field choice">'+
|
||||
'<span class="text"></span>'+
|
||||
'<select>'+
|
||||
'<option class="option"></option>'+
|
||||
'</select>'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return 'select' in val
|
||||
},
|
||||
set: function(field, value){
|
||||
var t = field.find('.text').text()
|
||||
var item = field.find('.option').last()
|
||||
var select = field.find('select')
|
||||
for(var i=0; i < value.select.length; i++){
|
||||
item
|
||||
.html(value.select[i])
|
||||
.val(value.select[i])
|
||||
item.appendTo(select)
|
||||
|
||||
item = item.clone()
|
||||
}
|
||||
if(value.default != null){
|
||||
if(typeof(value.default) == typeof(123)){
|
||||
field.find('.option')
|
||||
.eq(value.default)
|
||||
.attr('selected', '')
|
||||
} else {
|
||||
field.find('.option[value="'+ value.default +'"]')
|
||||
.attr('selected', '')
|
||||
}
|
||||
}
|
||||
},
|
||||
get: function(field){
|
||||
return $(field).find('.option:selected').val()
|
||||
},
|
||||
},
|
||||
|
||||
// NOTE: a button can have state...
|
||||
// format:
|
||||
// {
|
||||
// // click event handler...
|
||||
// button: <function>,
|
||||
// // optional, button text (default 'OK')...
|
||||
// text: <button-label>,
|
||||
// // optional, initial state setup...
|
||||
// default: <function>,
|
||||
// }
|
||||
button: {
|
||||
type: 'button',
|
||||
text: null,
|
||||
default: false,
|
||||
html: '<div class="field button">'+
|
||||
'<span class="text"></span>'+
|
||||
'<button class="button"></button>'+
|
||||
'</div>',
|
||||
test: function(val){
|
||||
return 'button' in val
|
||||
},
|
||||
set: function(field, value){
|
||||
var btn = $(field).find('button')
|
||||
.click(value.button)
|
||||
.html(value.text == null ? 'OK' : value.text)
|
||||
if('default' in value){
|
||||
value.default(btn)
|
||||
}
|
||||
},
|
||||
get: function(field){
|
||||
return $(field).attr('state')
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Constructors...
|
||||
*/
|
||||
|
||||
// Show a complex form dialog
|
||||
//
|
||||
// This will build a form and collect it's data on "accept" specified by
|
||||
// the config object...
|
||||
//
|
||||
// config format:
|
||||
// {
|
||||
// // simple field...
|
||||
// <field-description>: <default-value>,
|
||||
//
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// <field-description> and split in two with a "|" the section before will
|
||||
// show as the field text and the text after as the tooltip.
|
||||
// Example:
|
||||
// "field text | field tooltip..."
|
||||
//
|
||||
// field's default value determines it's type:
|
||||
// bool - checkbox
|
||||
// string - textarea
|
||||
//
|
||||
// see FIELD_TYPES for supported field types.
|
||||
//
|
||||
// NOTE: if btn is set to false explicitly then no button will be
|
||||
// rendered in the form dialog.
|
||||
// NOTE: to include a literal "|" in <field-description> just escape it
|
||||
// like this: "\|"
|
||||
//
|
||||
// XXX add form testing...
|
||||
// XXX add undefined field handling/reporting...
|
||||
function formDialog(root, message, config, btn, cls){
|
||||
cls = cls == null ? '' : cls
|
||||
btn = btn == null ? 'OK' : btn
|
||||
root = root == null ? $('.viewer') : root
|
||||
|
||||
var form = $('<div class="form"/>')
|
||||
var data = {}
|
||||
var res = $.Deferred()
|
||||
|
||||
// handle message and btn...
|
||||
if(message.trim().length > 0){
|
||||
setTextWithTooltip(message, $('<div class="text"/>'))
|
||||
.appendTo(form)
|
||||
}
|
||||
|
||||
// build the form...
|
||||
for(var t in config){
|
||||
var did_handling = false
|
||||
for(var f in FIELD_TYPES){
|
||||
if(FIELD_TYPES[f].test(config[t])){
|
||||
var field = FIELD_TYPES[f]
|
||||
var html = $(field.html)
|
||||
|
||||
// setup text and data...
|
||||
setTextWithTooltip(t, html.find('.text'), html)
|
||||
|
||||
if(field.set != null){
|
||||
field.set(html, config[t])
|
||||
}
|
||||
|
||||
if(field.get != null){
|
||||
// NOTE: this is here to isolate t and field.get values...
|
||||
// ...is there a better way???
|
||||
var _ = (function(title, getter){
|
||||
html.on('resolve', function(evt, e){
|
||||
data[title] = getter(e)
|
||||
})
|
||||
})(t, field.get)
|
||||
}
|
||||
|
||||
form.append(html)
|
||||
|
||||
did_handling = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// handle unresolved fields...
|
||||
if(!did_handling){
|
||||
console.warn('formDialog: not all fields understood.')
|
||||
// XXX skipping field...
|
||||
// XXX
|
||||
}
|
||||
}
|
||||
|
||||
// add button...
|
||||
if(btn !== false){
|
||||
var button = $('<button class="accept">'+btn+'</button>')
|
||||
form.append(button)
|
||||
} else {
|
||||
var button = null
|
||||
}
|
||||
|
||||
var overlay = showInOverlay(root, form)
|
||||
.addClass('dialog ' + cls)
|
||||
.on('accept', function(){
|
||||
form.find('.field').each(function(_, e){
|
||||
$(e).trigger('resolve', [$(e)])
|
||||
})
|
||||
|
||||
// XXX test if all required stuff is filled...
|
||||
res.resolve(data, form)
|
||||
|
||||
hideOverlay(root)
|
||||
})
|
||||
.on('close', function(){
|
||||
res.reject()
|
||||
|
||||
})
|
||||
|
||||
if(button != null){
|
||||
button.click(function(){
|
||||
overlay.trigger('accept')
|
||||
})
|
||||
}
|
||||
|
||||
// focus an element...
|
||||
// NOTE: if first element is a radio button set, focus the checked
|
||||
// element, else focus the first input...
|
||||
form.ready(function(){
|
||||
// NOTE: we are using a timeout to avoid the user input that opened
|
||||
// the dialog to end up in the first field...
|
||||
setTimeout(function(){
|
||||
var elem = form.find('.field input').first()
|
||||
if(elem.attr('type') == 'radio'){
|
||||
form.find('.field input:checked')
|
||||
.focus()
|
||||
.select()
|
||||
} else {
|
||||
elem
|
||||
.focus()
|
||||
.select()
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
|
||||
/************************************************ Standard dialogs ***/
|
||||
// NOTE: these return a deferred that will reflect the state of the
|
||||
// dialog, and the progress of the operations that it riggers...
|
||||
//
|
||||
// XXX might be a good idea to be able to block the ui (overlay + progress
|
||||
// bar?) until some long/critical operations finish, to prevent the
|
||||
// user from breaking things while the ui is inconsistent...
|
||||
|
||||
function alertDialog(){
|
||||
var message = $.makeArray(arguments).join(' ')
|
||||
return formDialog(null, String(message), {}, false, 'alert')
|
||||
}
|
||||
|
||||
|
||||
function promptDialog(message, dfl, btn){
|
||||
btn = btn == null ? 'OK' : btn
|
||||
var res = $.Deferred()
|
||||
formDialog(null, message, {'': ''+(dfl == null ? '' : dfl)}, btn, 'prompt')
|
||||
.done(function(data){ res.resolve(data['']) })
|
||||
.fail(function(){ res.reject() })
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
function confirmDialog(){
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
394
ui (gen4)/lib/editor.js
Executable file
394
ui (gen4)/lib/editor.js
Executable file
@ -0,0 +1,394 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
var DEFAULT_FILTER_ORDER = [
|
||||
// 'gamma',
|
||||
'brightness',
|
||||
'contrast',
|
||||
'saturate',
|
||||
'hue-rotate',
|
||||
'grayscale',
|
||||
'invert',
|
||||
'sepia'
|
||||
]
|
||||
|
||||
var SLIDER_SCALE = 43.47
|
||||
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
function r2v(r){
|
||||
return Math.pow(Math.E, r/SLIDER_SCALE)
|
||||
}
|
||||
|
||||
function v2r(v){
|
||||
return Math.log(v)*SLIDER_SCALE
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Update filter in target image...
|
||||
//
|
||||
function updateFilter(e, f, v, order){
|
||||
e = $(e)
|
||||
var state = e
|
||||
.css('-webkit-filter')
|
||||
state = state == 'none' ? '' : state + ' '
|
||||
// update existing filter...
|
||||
if(RegExp(f).test(state)){
|
||||
state = state.replace(RegExp(f+'\\s*\\([^\\)]*\\)'), f+'('+v+')')
|
||||
// add new filter...
|
||||
} else {
|
||||
state += f+'('+v+')'
|
||||
state = sortFilterStr(state, order)
|
||||
}
|
||||
e.css({
|
||||
'-webkit-filter': state,
|
||||
})
|
||||
return v
|
||||
}
|
||||
function resetFilter(e, f){
|
||||
e = $(e)
|
||||
var state = e
|
||||
.css('-webkit-filter')
|
||||
state = state == 'none' ? '' : state + ' '
|
||||
state = state.replace(RegExp(f+'\\s*\\([^\\)]*\\)'), '').trim()
|
||||
e.css({
|
||||
'-webkit-filter': state,
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
|
||||
function getSliderOrder(){
|
||||
return $('.filter-list').sortable('toArray')
|
||||
}
|
||||
// NOTE: this will return only the set filters...
|
||||
function getFilterOrder(target){
|
||||
return $(target)
|
||||
.css('-webkit-filter')
|
||||
.split(/\s*\([^\)]*\)\s*/g)
|
||||
.slice(0, -1)
|
||||
}
|
||||
|
||||
|
||||
function sortFilterStr(state, order){
|
||||
order = order == null ? getSliderOrder() : order
|
||||
state = state.split(/\s+/)
|
||||
state.sort(function(a, b){
|
||||
a = order.indexOf(a.replace(/\(.*/, ''))
|
||||
b = order.indexOf(b.replace(/\(.*/, ''))
|
||||
return a - b
|
||||
})
|
||||
return state.join(' ')
|
||||
}
|
||||
function sortFilterSliders(order){
|
||||
return $('.filter-list').sortChildren(function(a, b){
|
||||
a = order.indexOf(a.id)
|
||||
b = order.indexOf(b.id)
|
||||
return a - b
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Load state of sliders from target...
|
||||
//
|
||||
function loadSliderState(target){
|
||||
// break the feedback loop that if present will write the state
|
||||
// back...
|
||||
var filters = $('.filter-list input[type=range]')
|
||||
.prop('disabled', true)
|
||||
|
||||
var res = $(target)
|
||||
.css('-webkit-filter')
|
||||
var state = res
|
||||
.split(/\s*\(\s*|\s*\)\s*/g)
|
||||
.reverse()
|
||||
.slice(1)
|
||||
// reset sliders to defaults...
|
||||
$('input[type=range]').each(function(i, e){
|
||||
e = $(e)
|
||||
e.val(e.attr('default')).change()
|
||||
})
|
||||
// set the saved values...
|
||||
while(state.length > 0){
|
||||
var e = $('[filter='+state.pop()+']')
|
||||
if(e.prop('normalize')){
|
||||
e.val(v2r(parseFloat(state.pop()))).change()
|
||||
} else {
|
||||
e.val(parseFloat(state.pop())).change()
|
||||
}
|
||||
}
|
||||
|
||||
filters
|
||||
.prop('disabled', false)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
function saveSnapshot(target){
|
||||
var l = $('.state').last().text()
|
||||
l = l == '' ? 0 : parseInt(l)+1
|
||||
var state = $(target).css('-webkit-filter')
|
||||
$('<div/>')
|
||||
.text(l)
|
||||
.addClass('state')
|
||||
.attr({
|
||||
state: state,
|
||||
sliders: getSliderOrder().join(' ')
|
||||
})
|
||||
// load state...
|
||||
.click(function(){
|
||||
loadSliderState($(target).css('-webkit-filter', state))
|
||||
sortFilterSliders($(this).attr('sliders').split(' '))
|
||||
})
|
||||
.appendTo($('.states'))
|
||||
.draggable({
|
||||
revert: 'invalid',
|
||||
revertDuration: 200,
|
||||
})
|
||||
}
|
||||
function clearSnapshots(){
|
||||
$('.state').remove()
|
||||
}
|
||||
|
||||
|
||||
// Re-read filters form target image and reset the controls...
|
||||
//
|
||||
function reloadControls(target){
|
||||
clearSnapshots()
|
||||
var state = loadSliderState(target)
|
||||
|
||||
// nothing set -- default sort...
|
||||
if(state == 'none'){
|
||||
sortFilterSliders(DEFAULT_FILTER_ORDER)
|
||||
|
||||
// load existing sort state...
|
||||
} else {
|
||||
sortFilterSliders(getFilterOrder(target).concat(DEFAULT_FILTER_ORDER))
|
||||
}
|
||||
// make a snapshot...
|
||||
saveSnapshot(target)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Element constructors...
|
||||
*/
|
||||
|
||||
function makeAbsRange(text, filter, target, min, max, dfl, step, translate, normalize){
|
||||
min = min == null ? 0 : min
|
||||
max = max == null ? 1 : max
|
||||
dfl = dfl == null ? min : dfl
|
||||
step = step == null ? 0.01 : step
|
||||
translate = translate == null ? function(v){return v} : translate
|
||||
normalize = normalize == null ? false : true
|
||||
|
||||
var elem = $('<div class="control range"></div>')
|
||||
.attr({
|
||||
id: filter,
|
||||
})
|
||||
$('<span class="title"/>')
|
||||
.html(text)
|
||||
.appendTo(elem)
|
||||
// NOTE: the range element is the main "writer"...
|
||||
var range = $('<input class="slider" type="range">')
|
||||
.attr({
|
||||
filter: filter,
|
||||
min: min,
|
||||
max: max,
|
||||
step: step,
|
||||
default: dfl,
|
||||
})
|
||||
.prop('normalize', normalize)
|
||||
.val(dfl)
|
||||
.change(function(){
|
||||
var val = this.valueAsNumber
|
||||
value.val(val)
|
||||
if(!elem.prop('disabled') && !$(this).prop('disabled')){
|
||||
updateFilter(target, filter, translate(val))
|
||||
}
|
||||
if(parseFloat(val) == dfl){
|
||||
elem.addClass('at-default')
|
||||
} else {
|
||||
elem.removeClass('at-default')
|
||||
}
|
||||
})
|
||||
.appendTo(elem)
|
||||
var value = $('<input type="number" class="value"/>')
|
||||
.attr({
|
||||
min: min,
|
||||
max: max,
|
||||
step: step,
|
||||
})
|
||||
.val(dfl)
|
||||
.change(function(){
|
||||
range.val($(this).val()).change()
|
||||
})
|
||||
.appendTo(elem)
|
||||
$('<button class="reset">×</button>')
|
||||
.click(function(){
|
||||
range.val(dfl).change()
|
||||
resetFilter(target, filter)
|
||||
})
|
||||
.appendTo(elem)
|
||||
return elem
|
||||
}
|
||||
function makeLogRange(text, filter, target){
|
||||
return makeAbsRange(text, filter, target, -100, 100, 0, 0.1, r2v, true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Constructors...
|
||||
*/
|
||||
|
||||
function buildFilterUI(target){
|
||||
return $('<div>')
|
||||
.append($('<div class="filter-list"/>')
|
||||
//.append(makeLogRange('Gamma:', 'gamma', target))
|
||||
.append(makeLogRange('Brightness:', 'brightness', target))
|
||||
.append(makeLogRange('Contrast:', 'contrast', target))
|
||||
.append(makeLogRange('Saturation:', 'saturate', target))
|
||||
.append(makeAbsRange('Hue:', 'hue-rotate', target,
|
||||
-180, 180, 0, 0.5, function(v){ return v+'deg' }))
|
||||
.append(makeAbsRange('Grayscale:', 'grayscale', target))
|
||||
.append(makeAbsRange('Invert:', 'invert', target))
|
||||
.append(makeAbsRange('Sepia:', 'sepia', target))
|
||||
.sortable({
|
||||
axis: 'y',
|
||||
})
|
||||
.on('sortstop', function(){
|
||||
// update image filter order...
|
||||
var img = $(target)
|
||||
img.css('-webkit-filter', sortFilterStr(img.css('-webkit-filter')))
|
||||
}))
|
||||
.append($('<hr>'))
|
||||
.append('<span>Reset: <span>')
|
||||
.append($('<button>Values</button>')
|
||||
.click(function(){
|
||||
$('.reset').click()
|
||||
}))
|
||||
.append($('<button>Order</button>')
|
||||
.click(function(){
|
||||
sortFilterSliders(DEFAULT_FILTER_ORDER)
|
||||
}))
|
||||
.append($('<button>All</button>')
|
||||
.click(function(){
|
||||
$('.reset').click()
|
||||
sortFilterSliders(DEFAULT_FILTER_ORDER)
|
||||
}))
|
||||
.children()
|
||||
}
|
||||
|
||||
|
||||
function buildSnapshotsUI(target){
|
||||
return $('<div>')
|
||||
.append($('<div class="states"/>'))
|
||||
.append($('<hr>'))
|
||||
.append($('<button/>')
|
||||
.click(function(){ saveSnapshot(target) })
|
||||
.text('Save'))
|
||||
.append($('<button/>')
|
||||
.addClass('remove-state-drop-target')
|
||||
.click(function(){ clearSnapshots() })
|
||||
.text('Clear')
|
||||
.droppable({
|
||||
accept: '.state',
|
||||
activate: function(e, ui){
|
||||
$(this).text('Delete')
|
||||
},
|
||||
deactivate: function(e, ui){
|
||||
$(this).text('Clear')
|
||||
},
|
||||
drop: function(e, ui){
|
||||
ui.helper.remove()
|
||||
}
|
||||
|
||||
}))
|
||||
.children()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Panels...
|
||||
*/
|
||||
|
||||
Panel('Edit: Filters',
|
||||
// build UI...
|
||||
function(){
|
||||
// XXX hardcoded target is not good...
|
||||
return buildFilterUI('.current.image')
|
||||
},
|
||||
// setup...
|
||||
function(panel){
|
||||
// NOTE: we need to have this in the namespace so as to be able
|
||||
// to both register and drop event handlers...
|
||||
var _editorUpdateor = function(){
|
||||
reloadControls('.current.image')
|
||||
}
|
||||
|
||||
panel
|
||||
.on('panelOpening', function(){
|
||||
// register updater...
|
||||
$('.viewer')
|
||||
.on('focusingImage', _editorUpdateor)
|
||||
// update the editor state in case the target changed...
|
||||
_editorUpdateor()
|
||||
})
|
||||
.on('panelClosing', function(){
|
||||
// unregister updater...
|
||||
$('.viewer')
|
||||
.off('focusingImage', _editorUpdateor)
|
||||
})
|
||||
|
||||
/*
|
||||
// XXX a different approach...
|
||||
// XXX not yet sure which approach is better...
|
||||
// XXX this has one draw back -- the handler is allways there...
|
||||
// ...depending on how fast isPanelVisible(..) is, this might
|
||||
// not be a problem, especially if the panel is also allways
|
||||
// there...
|
||||
var _editorUpdateor = function(){
|
||||
if(isPanelVisible(panel)){
|
||||
reloadControls('.current.image')
|
||||
}
|
||||
}
|
||||
$('.viewer')
|
||||
.on('focusingImage', _editorUpdateor)
|
||||
|
||||
panel
|
||||
.on('panelOpening', function(){
|
||||
// update the editor state in case the target changed...
|
||||
_editorUpdateor()
|
||||
})
|
||||
*/
|
||||
},
|
||||
true)
|
||||
|
||||
|
||||
Panel('Edit: Snapshots',
|
||||
// build UI...
|
||||
function(){
|
||||
// XXX hardcoded target is not good...
|
||||
return buildSnapshotsUI('.current.image')
|
||||
},
|
||||
// setup...
|
||||
function(panel){
|
||||
// XXX
|
||||
},
|
||||
true)
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set sw=4 ts=4 : */
|
||||
1229
ui (gen4)/lib/jli.js
Executable file
1229
ui (gen4)/lib/jli.js
Executable file
File diff suppressed because it is too large
Load Diff
795
ui (gen4)/lib/keyboard.js
Executable file
795
ui (gen4)/lib/keyboard.js
Executable file
@ -0,0 +1,795 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Attributes to be ignored my the key handler...
|
||||
//
|
||||
// These are used for system tasks.
|
||||
var KEYBOARD_SYSTEM_ATTRS = [
|
||||
'doc',
|
||||
'title',
|
||||
'ignore',
|
||||
'pattern'
|
||||
]
|
||||
|
||||
|
||||
// Neither _SPECIAL_KEYS nor _KEY_CODES are meant for direct access, use
|
||||
// toKeyName(<code>) and toKeyCode(<name>) for a more uniform access.
|
||||
//
|
||||
// NOTE: these are un-shifted ASCII key names rather than actual key
|
||||
// code translations.
|
||||
// NOTE: ASCII letters (capital) are not present because they actually
|
||||
// match their key codes and are accessible via:
|
||||
// String.fromCharCode(<code>) or <letter>.charCodeAt(0)
|
||||
// NOTE: the lower case letters are accessible by adding 32 to the
|
||||
// capital key code.
|
||||
// NOTE: don't understand why am I the one who has to write this...
|
||||
var _SPECIAL_KEYS = {
|
||||
// Special Keys...
|
||||
9: 'Tab', 33: 'PgUp', 45: 'Ins',
|
||||
13: 'Enter', 34: 'PgDown', 46: 'Del',
|
||||
16: 'Shift', 35: 'End', 8: 'Backspace',
|
||||
17: 'Ctrl', 36: 'Home', 91: 'Win',
|
||||
18: 'Alt', 37: 'Left', 93: 'Menu',
|
||||
20: 'Caps Lock',38: 'Up',
|
||||
27: 'Esc', 39: 'Right',
|
||||
32: 'Space', 40: 'Down',
|
||||
|
||||
// Function Keys...
|
||||
112: 'F1', 116: 'F5', 120: 'F9',
|
||||
113: 'F2', 117: 'F6', 121: 'F10',
|
||||
114: 'F3', 118: 'F7', 122: 'F11',
|
||||
115: 'F4', 119: 'F8', 123: 'F12',
|
||||
|
||||
// Number row..
|
||||
// NOTE: to avoid conflicts with keys that have a code the same as
|
||||
// the value of a number key...
|
||||
// Ex:
|
||||
// 'Backspace' (8) vs. '8' (56)
|
||||
// 'Tab' (9) vs. '9' (57)
|
||||
// ...all of the numbers start with a '#'
|
||||
// this is a problem due to JS coercing the types to string
|
||||
// on object attr access.
|
||||
// Ex:
|
||||
// o = {1: 2}
|
||||
// o[1] == o['1'] == true
|
||||
49: '#1', 50: '#2', 51: '#3', 52: '#4', 53: '#5',
|
||||
54: '#6', 55: '#7', 56: '#8', 57: '#9', 48: '#0',
|
||||
|
||||
// Punctuation...
|
||||
// top row...
|
||||
192: '`', /* Numbers */ 189: '-', 187: '=',
|
||||
// right side of keyboard...
|
||||
219: '[', 221: ']', 220: '\\',
|
||||
186: ';', 222: '\'',
|
||||
188: ',', 190: '.', 191: '/',
|
||||
}
|
||||
|
||||
|
||||
var _SHIFT_KEYS = {
|
||||
'`': '~', '-': '_', '=':'+',
|
||||
|
||||
1: '!', 2: '@', 3: '#', 4: '$', 5: '%',
|
||||
6:'^', 7:'&', 8: '*', 9: '(', 0: ')',
|
||||
|
||||
'[': '{', ']': '}', '\\': '|',
|
||||
';': ':', '\'': '"',
|
||||
',': '<', '.': '>', '/': '?'
|
||||
}
|
||||
|
||||
|
||||
// build a reverse map of _SPECIAL_KEYS
|
||||
var _KEY_CODES = {}
|
||||
for(var k in _SPECIAL_KEYS){
|
||||
_KEY_CODES[_SPECIAL_KEYS[k]] = k
|
||||
}
|
||||
|
||||
|
||||
// XXX some keys look really wrong...
|
||||
function toKeyName(code){
|
||||
// check for special keys...
|
||||
var k = _SPECIAL_KEYS[code]
|
||||
if(k != null){
|
||||
return k
|
||||
}
|
||||
// chars...
|
||||
k = String.fromCharCode(code)
|
||||
if(k != ''){
|
||||
//return k.toLowerCase()
|
||||
return k
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
function toKeyCode(c){
|
||||
if(c in _KEY_CODES){
|
||||
return _KEY_CODES[c]
|
||||
}
|
||||
return c.charCodeAt(0)
|
||||
}
|
||||
|
||||
|
||||
// documentation wrapper...
|
||||
function doc(text, func){
|
||||
func = !func ? function(){return true}: func
|
||||
func.doc = text
|
||||
return func
|
||||
}
|
||||
|
||||
|
||||
// Build or normalize a modifier string.
|
||||
//
|
||||
// Acceptable argument sets:
|
||||
// - none -> ""
|
||||
// - true, false, true -> "ctrl+shift"
|
||||
// - true, false -> "ctrl"
|
||||
// - [true, false] -> "ctrl"
|
||||
// - 'alt+shift' -> "alt+shift"
|
||||
// - 'shift - alt' -> "alt+shift"
|
||||
//
|
||||
function normalizeModifiers(c, a, s){
|
||||
if(c != null && c.constructor.name == 'Array'){
|
||||
a = c[1]
|
||||
s = c[2]
|
||||
c = c[0]
|
||||
}
|
||||
if(typeof(c) == typeof('str')){
|
||||
var modifiers = c
|
||||
} else {
|
||||
var modifiers = (c ? 'ctrl' : '')
|
||||
+ (a ? ' alt' : '')
|
||||
+ (s ? ' shift' : '')
|
||||
}
|
||||
|
||||
// build the dormalized modifier string...
|
||||
var res = /ctrl/i.test(modifiers) ? 'ctrl' : ''
|
||||
res += /alt/i.test(modifiers) ? (res != '' ? '+alt' : 'alt') : ''
|
||||
res += /shift/i.test(modifiers) ? (res != '' ? '+shift' : 'shift') : ''
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
/* Key handler getter
|
||||
*
|
||||
* For doc on format see makeKeyboardHandler(...)
|
||||
*
|
||||
* modifiers can be:
|
||||
* - '' (default) - No modifiers
|
||||
* - '?' - Return list of applicable modifiers per mode
|
||||
* - <modifier> - Any of 'ctrl', 'alt' or 'shift' alone or in
|
||||
* combination.
|
||||
* Combinations MUST be ordered as shown above.
|
||||
* Combination elements are separated by '+'
|
||||
* Ex:
|
||||
* 'ctrl+shift'
|
||||
* NOTE: 'shift+ctrl' is wrong.
|
||||
* NOTE: normalizeModifiers(...) can be used as
|
||||
* a reference, if in doubt.
|
||||
*
|
||||
* modes can be:
|
||||
* - 'any' (default) - Get list of all applicable handlers up until
|
||||
* the first applicable ignore.
|
||||
* - 'all' - Get ALL handlers, including ignores
|
||||
* - <mode> - Get handlers for an explicit mode
|
||||
*
|
||||
*
|
||||
* This will also resolve several shifted keys by name, for example:
|
||||
* 'shift-/' is the same as '?', and either can be used, but the shorter
|
||||
* direct notation has priority (see _SHIFT_KEYS for supported keys).
|
||||
*
|
||||
*
|
||||
* Returns:
|
||||
* {
|
||||
* <mode>: <handler>,
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
*
|
||||
* <handler> can be:
|
||||
* - <function> - handler
|
||||
* - [<doc>, <function>]
|
||||
* - lisp-style handler
|
||||
* - 'IGNORE' - if mode is 'all' and key is in .ignore
|
||||
* - [<function>, 'IGNORE NEXT']
|
||||
* - if mode is 'all' and the key is both in .ignore
|
||||
* and a handler is defined in the current section
|
||||
* NOTE: in this case if this mode matches, all
|
||||
* the subsequent handlers will get ignored
|
||||
* in normal modes...
|
||||
*
|
||||
*
|
||||
* NOTE: adding a key to the ignore list has the same effect as returning
|
||||
* false form it's handler in the same context.
|
||||
* NOTE: it is not possible to do a shift-? as it is already shifted.
|
||||
* NOTE: if a key is not handled in a mode, that mode will not be
|
||||
* present in the resulting object.
|
||||
* NOTE: this will not unwrap lisp-style (see below) handlers.
|
||||
* NOTE: modes are prioritized by order of occurrence.
|
||||
* NOTE: modifiers can be a list of three bools...
|
||||
* (see: normalizeModifiers(...) for further information)
|
||||
*
|
||||
* XXX check do we need did_handling here...
|
||||
* XXX BUG explicitly given modes do not yield results if the pattern
|
||||
* does not match...
|
||||
*/
|
||||
function getKeyHandlers(key, modifiers, keybindings, modes, shifted_keys){
|
||||
var chr = null
|
||||
var s_chr = null
|
||||
// XXX I do not understand why this is here...
|
||||
var did_handling = false
|
||||
var did_ignore = false
|
||||
modifiers = modifiers == null ? '' : modifiers
|
||||
modifiers = modifiers != '?' ? normalizeModifiers(modifiers) : modifiers
|
||||
modes = modes == null ? 'any' : modes
|
||||
shifted_keys = shifted_keys == null ? _SHIFT_KEYS : shifted_keys
|
||||
|
||||
if(typeof(key) == typeof(123)){
|
||||
key = key
|
||||
chr = toKeyName(key)
|
||||
} else {
|
||||
chr = key
|
||||
key = toKeyCode(key)
|
||||
}
|
||||
|
||||
// XXX this is not done yet...
|
||||
if(shifted_keys != false && /shift/i.test(modifiers)){
|
||||
var s_chr = shifted_keys[chr]
|
||||
}
|
||||
|
||||
res = {}
|
||||
|
||||
for(var title in keybindings){
|
||||
|
||||
// If a key is ignored then look no further...
|
||||
if(did_ignore){
|
||||
if(modes != 'all'){
|
||||
break
|
||||
} else {
|
||||
did_ignore = false
|
||||
if(modifiers != '?' && res[mode] != 'IGNORE'){
|
||||
res[mode] = [ res[mode], 'IGNORE NEXT']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// older version compatibility...
|
||||
if(keybindings[title].pattern != null){
|
||||
var mode = keybindings[title].pattern
|
||||
} else {
|
||||
var mode = title
|
||||
}
|
||||
|
||||
// check if we need to skip this mode...
|
||||
if( !(modes == 'all'
|
||||
// explicit mode match...
|
||||
|| modes == mode
|
||||
// 'any' means we need to check the mode...
|
||||
|| (modes == 'any'
|
||||
// '*' always matches...
|
||||
&& mode == '*'
|
||||
// match the mode...
|
||||
|| $(mode).length != 0))){
|
||||
continue
|
||||
}
|
||||
|
||||
var bindings = keybindings[title]
|
||||
|
||||
if(s_chr != null && s_chr in bindings){
|
||||
var handler = bindings[s_chr]
|
||||
chr = s_chr
|
||||
modifiers = modifiers.replace(/\+?shift/i, '')
|
||||
} else if(chr in bindings){
|
||||
var handler = bindings[chr]
|
||||
} else {
|
||||
var handler = bindings[key]
|
||||
}
|
||||
|
||||
// alias...
|
||||
// XXX should this be before after or combined with ignore handling...
|
||||
while( handler != null
|
||||
&& (typeof(handler) == typeof(123)
|
||||
|| typeof(handler) == typeof('str')
|
||||
|| typeof(handler) == typeof({})
|
||||
&& handler.constructor.name == 'Object') ){
|
||||
|
||||
// do the complex handler aliases...
|
||||
if(typeof(handler) == typeof({}) && handler.constructor.name == 'Object'){
|
||||
// build modifier list...
|
||||
if(modifiers == '?'){
|
||||
break
|
||||
}
|
||||
if(modifiers in handler){
|
||||
if(typeof(handler[modifiers]) == typeof('str')){
|
||||
handler = handler[modifiers]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if(typeof(handler['default']) == typeof('str')){
|
||||
handler = handler['default']
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// simple handlers...
|
||||
if(handler in bindings){
|
||||
// XXX need to take care of that we can always be a number or a string...
|
||||
handler = bindings[handler]
|
||||
} else if(typeof(handler) == typeof(1)) {
|
||||
handler = bindings[toKeyName(handler)]
|
||||
} else {
|
||||
handler = bindings[toKeyCode(handler)]
|
||||
}
|
||||
}
|
||||
|
||||
// if something is ignored then just breakout and stop handling...
|
||||
if(bindings.ignore == '*'
|
||||
|| bindings.ignore != null
|
||||
&& (bindings.ignore.indexOf(key) != -1
|
||||
|| bindings.ignore.indexOf(chr) != -1)){
|
||||
did_handling = true
|
||||
// ignoring a key will stop processing it...
|
||||
if(modes == 'all' || mode == modes){
|
||||
// NOTE: if a handler is defined in this section, this
|
||||
// will be overwritten...
|
||||
// XXX need to add the handler to this if it's defined...
|
||||
res[mode] = 'IGNORE'
|
||||
}
|
||||
did_ignore = true
|
||||
}
|
||||
|
||||
// no handler...
|
||||
if(handler == null){
|
||||
continue
|
||||
}
|
||||
|
||||
// complex handler...
|
||||
if(typeof(handler) == typeof({}) && handler.constructor.name == 'Object'){
|
||||
// build modifier list...
|
||||
if(modifiers == '?'){
|
||||
res[mode] = Object.keys(handler)
|
||||
did_handling = true
|
||||
continue
|
||||
}
|
||||
|
||||
var callback = handler[modifiers]
|
||||
if(callback == null){
|
||||
callback = handler['default']
|
||||
}
|
||||
|
||||
if(callback != null){
|
||||
res[mode] = callback
|
||||
|
||||
did_handling = true
|
||||
continue
|
||||
}
|
||||
|
||||
// simple callback...
|
||||
} else {
|
||||
// build modifier list...
|
||||
if(modifiers == '?'){
|
||||
res[mode] = 'none'
|
||||
} else {
|
||||
res[mode] = handler
|
||||
}
|
||||
|
||||
did_handling = true
|
||||
continue
|
||||
}
|
||||
|
||||
if(modes != 'all' && did_handling){
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
/* Basic key binding format:
|
||||
*
|
||||
* {
|
||||
* <title>: {
|
||||
* doc: <text>,
|
||||
* pattern: <css-selector>,
|
||||
*
|
||||
* // this defines the list of keys to ignore by the handler.
|
||||
* // NOTE: use "*" to ignore all keys other than explicitly
|
||||
* // defined in the current section.
|
||||
* // NOTE: ignoring a key will stop processing it in other
|
||||
* // compatible modes.
|
||||
* ignore: <ignored-keys>
|
||||
*
|
||||
* // NOTE: a callback can have a .doc attr containing
|
||||
* // documentation...
|
||||
* <key-def> : <callback>,
|
||||
*
|
||||
* <key-def> : [
|
||||
* // this can be any type of handler except for an alias...
|
||||
* <handler>,
|
||||
* <doc>
|
||||
* ],
|
||||
*
|
||||
* <key-def> : {
|
||||
* // modifiers can either have a callback or an alias as
|
||||
* // a value...
|
||||
* // NOTE: when the alias is resolved, the same modifiers
|
||||
* // will be applied to the final resolved handler.
|
||||
* default: <callback> | <key-def-x>,
|
||||
*
|
||||
* // a modifier can be any single modifier, like shift or a
|
||||
* // combination of modifiers like 'ctrl+shift', in order
|
||||
* // of priority.
|
||||
* // supported modifiers, ordered by priority, are:
|
||||
* // - ctrl
|
||||
* // - alt
|
||||
* // - shift
|
||||
* // NOTE: if in doubt use normalizeModifiers(..) as a
|
||||
* // reference...
|
||||
* <modifer>: [...],
|
||||
* ...
|
||||
* },
|
||||
*
|
||||
* // alias...
|
||||
* <key-def-a> : <key-def-b>,
|
||||
*
|
||||
* ...
|
||||
* },
|
||||
*
|
||||
* // legacy format, still supported... (deprecated)
|
||||
* <css-selector>: {
|
||||
* // meta-data used to generate user docs/help/config
|
||||
* title: <text>,
|
||||
* ...
|
||||
* },
|
||||
*
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
*
|
||||
* <key-def> can be:
|
||||
* - explicit key code, e.g. 65
|
||||
* - key name, if present in _SPECIAL_KEYS, e.g. Enter
|
||||
* - key char (uppercase), as is returned by String.fromCharCode(...) e.g. A
|
||||
* - action -- any arbitrary string that is not in the above categories.
|
||||
*
|
||||
*
|
||||
* NOTE: adding a key to the ignore list has the same effect as returning
|
||||
* false form it's handler in the same context.
|
||||
* NOTE: actions,the last case, are used for alias referencing, they will
|
||||
* never match a real key, but will get resolved in alias searches.
|
||||
* NOTE: to test what to use as <key-def> use toKeyCode(..) / toKeyName(..).
|
||||
* NOTE: all fields are optional.
|
||||
* NOTE: if a handler explicitly returns false then that will break the
|
||||
* event propagation chain and exit the handler.
|
||||
* i.e. no other matching handlers will be called.
|
||||
* NOTE: if more than one match is found all matching handlers will be
|
||||
* called in sequence until one returns false explicitly.
|
||||
* NOTE: a <css-selector> is used as a predicate to select a section to
|
||||
* use. if multiple selectors match something then multiple sections
|
||||
* will be resolved in order of occurrence.
|
||||
* NOTE: the number keys are named with a leading hash '#' (e.g. '#8')
|
||||
* to avoid conflicsts with keys that have the code with the same
|
||||
* value (e.g. 'backspace' (8)).
|
||||
* NOTE: one can use a doc(<doc-string>, <callback>) as a shorthand to
|
||||
* assign a docstring to a handler.
|
||||
* it will only assign .doc attr and return the original function.
|
||||
*
|
||||
* XXX need an explicit way to prioritize modes...
|
||||
* XXX will aliases get resolved if they are in a different mode??
|
||||
*/
|
||||
function makeKeyboardHandler(keybindings, unhandled){
|
||||
if(unhandled == null){
|
||||
unhandled = function(){}
|
||||
}
|
||||
return function(evt){
|
||||
var did_handling = false
|
||||
var res = null
|
||||
|
||||
// key data...
|
||||
var key = evt.keyCode
|
||||
|
||||
// get modifiers...
|
||||
var modifiers = [evt.ctrlKey, evt.altKey, evt.shiftKey]
|
||||
|
||||
//window.DEBUG && console.log('KEY:', key, chr, modifiers)
|
||||
|
||||
var handlers = getKeyHandlers(key, modifiers, keybindings)
|
||||
|
||||
for(var mode in handlers){
|
||||
var handler = handlers[mode]
|
||||
if(handler != null){
|
||||
|
||||
// Array, lisp style with docs...
|
||||
if(typeof(handler) == typeof([]) && handler.constructor.name == 'Array'){
|
||||
// we do not care about docs here, so just get the handler...
|
||||
handler = handler[0]
|
||||
}
|
||||
|
||||
did_handling = true
|
||||
res = handler(evt)
|
||||
|
||||
if(res === false){
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!did_handling){
|
||||
return unhandled(key)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Build structure ready for conversion to HTML help.
|
||||
*
|
||||
* Structure:
|
||||
* {
|
||||
* <section-title>: {
|
||||
* doc: ...
|
||||
*
|
||||
* <handler-doc>: <keys-spec>
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* <keys-spec> - list of key names.
|
||||
*
|
||||
*
|
||||
* NOTE: this will not add keys (key names) that are not explicit key names.
|
||||
*/
|
||||
// XXX do we need to normalize/pre-process keybindings???
|
||||
// - might be a good idea to normalize the <modifiers>...
|
||||
function buildKeybindingsHelp(keybindings, shifted_keys){
|
||||
shifted_keys = shifted_keys == null ? _SHIFT_KEYS : shifted_keys
|
||||
var res = {}
|
||||
var mode, title
|
||||
|
||||
for(var title in keybindings){
|
||||
mode = keybindings[title]
|
||||
|
||||
// older version compatibility...
|
||||
if(keybindings[title].pattern != null){
|
||||
var pattern = keybindings[title].pattern
|
||||
} else {
|
||||
var pattern = title
|
||||
// titles and docs...
|
||||
var title = mode.title == null ? pattern : mode.title
|
||||
}
|
||||
|
||||
res[title] = {
|
||||
doc: mode.doc == null ? '' : mode.doc
|
||||
}
|
||||
section = res[title]
|
||||
|
||||
// handlers...
|
||||
for(var key in mode){
|
||||
if(KEYBOARD_SYSTEM_ATTRS.indexOf(key) >= 0){
|
||||
continue
|
||||
}
|
||||
var modifiers = getKeyHandlers(key, '?', keybindings, 'all')[pattern]
|
||||
modifiers = modifiers == 'none' || modifiers == undefined ? [''] : modifiers
|
||||
|
||||
for(var i=0; i < modifiers.length; i++){
|
||||
var mod = modifiers[i]
|
||||
|
||||
var handler = getKeyHandlers(key, mod, keybindings, 'all')[pattern]
|
||||
|
||||
if(handler.constructor.name == 'Array' && handler[1] == 'IGNORE NEXT'){
|
||||
handler = handler[0]
|
||||
}
|
||||
|
||||
// standard object doc...
|
||||
if('doc' in handler){
|
||||
var doc = handler.doc
|
||||
|
||||
// lisp style...
|
||||
} else if(handler.constructor.name == 'Array'){
|
||||
var doc = handler[1]
|
||||
|
||||
// no doc...
|
||||
} else {
|
||||
if('name' in handler && handler.name != ''){
|
||||
var doc = handler.name
|
||||
} else {
|
||||
// XXX is this the right way to do this?
|
||||
var doc = handler
|
||||
}
|
||||
}
|
||||
|
||||
// populate the section...
|
||||
// NOTE: we need a list of keys per action...
|
||||
if(doc in section){
|
||||
var keys = section[doc]
|
||||
} else {
|
||||
var keys = []
|
||||
section[doc] = keys
|
||||
}
|
||||
|
||||
// translate shifted keys...
|
||||
if(shifted_keys != false && mod == 'shift' && key in shifted_keys){
|
||||
mod = ''
|
||||
key = shifted_keys[key]
|
||||
}
|
||||
|
||||
// skip anything that is not a key...
|
||||
if(key.length > 1 && !(key in _KEY_CODES)){
|
||||
continue
|
||||
}
|
||||
|
||||
keys.push((mod == '' || mod == 'default') ? key : (mod +'+'+ key))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
// Get a list of keys associated with a given doc...
|
||||
//
|
||||
// The second argument must be a structure formated as returned by
|
||||
// buildKeybindingsHelp(...)
|
||||
//
|
||||
// Returned format:
|
||||
// {
|
||||
// <section-name> : <key-spec>
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// NOTE: <key-spec> is the same as generated by buildKeybindingsHelp(..)
|
||||
function getKeysByDoc(doc, help){
|
||||
var res = {}
|
||||
for(var mode in help){
|
||||
var name = mode
|
||||
var section = help[mode]
|
||||
if(doc in section){
|
||||
res[mode] = section[doc]
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
// Build a basic HTML table with keyboard help...
|
||||
//
|
||||
// The table will look like this:
|
||||
//
|
||||
// <table class="keyboard-help">
|
||||
//
|
||||
// <!-- section head -->
|
||||
// <tr class="section-title">
|
||||
// <th colspan=2> SECTION TITLE <th>
|
||||
// </tr>
|
||||
// <tr class="section-doc">
|
||||
// <td colspan=2> SECTION DESCRIPTION <td>
|
||||
// </tr>
|
||||
//
|
||||
// <!-- section keys -->
|
||||
// <tr>
|
||||
// <td> KEYS <td>
|
||||
// <td> ACTION DESCRIPTION <td>
|
||||
// </tr>
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// </table>
|
||||
//
|
||||
// NOTE: section are not separated in any way other than the <th> element.
|
||||
// NOTE: the actual HTML is created by jQuery, so the table may get
|
||||
// slightly structurally changed, i.e. a <tbody> element will be
|
||||
// added etc.
|
||||
//
|
||||
function buildKeybindingsHelpHTML(keybindings){
|
||||
var doc = buildKeybindingsHelp(keybindings)
|
||||
|
||||
var res = '<table class="keyboard-help">'
|
||||
for(var mode in doc){
|
||||
if(mode == 'doc'){
|
||||
continue
|
||||
}
|
||||
// section head...
|
||||
res += ' <tr class="section-title"><th colspan=2>' + mode + '</th></tr>\n'
|
||||
mode = doc[mode]
|
||||
res += ' <tr class="section-doc"><td colspan=2>'+ mode.doc + '</td></tr>\n'
|
||||
|
||||
// keys...
|
||||
for(var action in mode){
|
||||
if(action == 'doc'){
|
||||
continue
|
||||
}
|
||||
res += ' <tr><td>' + mode[action].join(', ') +'</td><td>'+ action + '</td></tr>\n'
|
||||
}
|
||||
}
|
||||
res += '</table>'
|
||||
|
||||
return $(res)
|
||||
}
|
||||
|
||||
|
||||
// Build HTML for a single key definition...
|
||||
//
|
||||
// Format if combining sections (default):
|
||||
// <span class="key-doc">
|
||||
// <span class="doc"> DOC </span>
|
||||
// <span class="keys"> KEYS </span>
|
||||
// </span>
|
||||
//
|
||||
// Format if not combining sections:
|
||||
// <span class="key-doc">
|
||||
// <span class="doc"> DOC </span>
|
||||
// <span class="section">
|
||||
// <span class="name"> MODE NAME </span>
|
||||
// <span class="keys"> KEYS </span>
|
||||
// </span>
|
||||
// ...
|
||||
// </span>
|
||||
//
|
||||
// XXX not yet sure if we are handling the sections correctly...
|
||||
function getKeysByDocHTML(doc, help, combine_sections){
|
||||
combine_sections = combine_sections == null ? true : combine_sections
|
||||
|
||||
var spec = getKeysByDoc(doc, help)
|
||||
var res = '<span class="key-doc">'
|
||||
|
||||
res += '<span class="doc">'+ doc +'</span>'
|
||||
|
||||
var keys = []
|
||||
|
||||
for(var section in spec){
|
||||
if(!combine_sections){
|
||||
keys = spec[section].join(', ')
|
||||
res += '<span class="section">'
|
||||
+'<span class="name">'+ section +'</span>'
|
||||
+'<span class="keys">'+ keys +'</span>'
|
||||
+'</span>'
|
||||
} else {
|
||||
keys = keys.concat(spec[section])
|
||||
}
|
||||
}
|
||||
|
||||
if(combine_sections){
|
||||
res += '<span class="keys">'+ keys.join(', ') +'</span>'
|
||||
}
|
||||
|
||||
return res + '</span>'
|
||||
}
|
||||
|
||||
|
||||
// Update key definitions...
|
||||
//
|
||||
// NOTE: this does not support multiple sections...
|
||||
function updateHTMLKeyDoc(help, root){
|
||||
root = root == null ? $('body') : root
|
||||
return root.find('.key-doc').each(function(i, e){
|
||||
e = $(e)
|
||||
var doc = e.find('.doc')
|
||||
var keys = $(getKeysByDocHTML(doc.html(), help)).find('.keys')
|
||||
e.find('.keys').html(keys.html())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Key binding editor...
|
||||
*/
|
||||
|
||||
// XXX
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
670
ui (gen4)/lib/panels.js
Executable file
670
ui (gen4)/lib/panels.js
Executable file
@ -0,0 +1,670 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
// this is an element/selector to be used as the temporary parent for
|
||||
// helpers while dragging/sorting sub-panels...
|
||||
// if set to null, the parent of a nearest panel will be used (slower)
|
||||
var PANEL_ROOT = 'body'
|
||||
|
||||
var PANEL_HELPER_HIDE_DELAY = 50
|
||||
var PANEL_HELPER_HIDE_DELAY_NO_ROOT = 100
|
||||
|
||||
|
||||
// Panel controller registry...
|
||||
//
|
||||
// Format:
|
||||
// {
|
||||
// <title>: <controller>,
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// The controller is generated by Panel(...) and is called
|
||||
// automatically by openPanel(...)
|
||||
var PANELS = {}
|
||||
|
||||
|
||||
// XXX write real doc...
|
||||
// XXX see getPanelState(...)
|
||||
// XXX we should keep track of panel state while moving, opening, closing
|
||||
// and resizing panels...
|
||||
// XXX move this to config???
|
||||
var PANEL_STATE = {}
|
||||
|
||||
|
||||
/*
|
||||
// This can be:
|
||||
// - hide
|
||||
// - remove
|
||||
var PANEL_CLOSE_METHOD = 'hide'
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Helpers...
|
||||
*/
|
||||
|
||||
// - start monitoring where we are dragged to...
|
||||
// - open hidden side panels...
|
||||
// XXX store number of panels we started with...
|
||||
function _startSortHandler(e, ui){
|
||||
ui.item.data('isoutside', false)
|
||||
ui.item.data('sub-panels-before', $(this).find('.sub-panel').length)
|
||||
ui.placeholder
|
||||
.height(ui.helper.outerHeight())
|
||||
.width(ui.helper.outerWidth())
|
||||
// show all hidden panels...
|
||||
$('.side-panel').each(function(){
|
||||
var p = $(this)
|
||||
if(p.find('.sub-panel').length == 0){
|
||||
p.css('min-width', '50px')
|
||||
}
|
||||
if(p.attr('autohide') == 'on'){
|
||||
p.attr('autohide', 'off')
|
||||
p.data('autohide', true)
|
||||
} else {
|
||||
p.data('autohide', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// reset the auto-hide of the side panels...
|
||||
function _resetSidePanels(){
|
||||
$('.side-panel').each(function(){
|
||||
var p = $(this)
|
||||
p.css('min-width', '')
|
||||
if(p.data('autohide')){
|
||||
p.attr('autohide', 'on')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function _prepareHelper(evt, elem){
|
||||
var offset = elem.offset()
|
||||
var w = elem.width()
|
||||
var h = elem.height()
|
||||
var root = elem.parents('.panel, .side-panel').first().parent()
|
||||
elem
|
||||
.detach()
|
||||
.css({
|
||||
position: 'absolute',
|
||||
width: w,
|
||||
height: h,
|
||||
})
|
||||
.offset(offset)
|
||||
.appendTo(root)
|
||||
return elem
|
||||
}
|
||||
|
||||
|
||||
function _resetSortedElem(elem){
|
||||
return elem
|
||||
.css({
|
||||
position: '',
|
||||
width: '',
|
||||
height: '',
|
||||
top: '',
|
||||
left: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// XXX add visibility test here...
|
||||
function isPanelVisible(panel){
|
||||
return panel.prop('open')
|
||||
&& (panel.parents('.panel').prop('open')
|
||||
|| panel.parents('.side-panel').width() > 20)
|
||||
}
|
||||
|
||||
|
||||
// wrap a sub-panel with a new panel...
|
||||
//
|
||||
function wrapWithPanel(panel, parent, offset){
|
||||
var new_panel = makePanel()
|
||||
.css(offset)
|
||||
.appendTo(parent)
|
||||
new_panel.find('.panel-content')
|
||||
.append(panel)
|
||||
return new_panel
|
||||
}
|
||||
|
||||
|
||||
function getPanel(title){
|
||||
return $('[id="'+ title +'"]')
|
||||
}
|
||||
|
||||
|
||||
function blinkPanel(panel){
|
||||
panel
|
||||
.addClass('blink')
|
||||
setTimeout(function(){
|
||||
panel.removeClass('blink')
|
||||
}, 170)
|
||||
return panel
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Constructors...
|
||||
*/
|
||||
|
||||
// XXX dragging out, into another panel and back out behaves oddly:
|
||||
// should:
|
||||
// either revert or create a new panel
|
||||
// does:
|
||||
// drops to last placeholder
|
||||
// XXX need to stop this triggering panelClosing event when the last
|
||||
// panel is dragged out or when the panel is dragged...
|
||||
function makePanel(title, parent, open, keep_empty, close_button){
|
||||
title = title == null || title.trim() == '' ? ' ' : title
|
||||
parent = parent == null ? $(PANEL_ROOT) : parent
|
||||
close_button = close_button == null ? true : close_button
|
||||
|
||||
// the outer panel...
|
||||
var panel = $('<details/>')
|
||||
.prop('open', open == null ? true : open)
|
||||
.addClass('panel noScroll')
|
||||
// NOTE: this is split into a separate event so as to be able to
|
||||
// be accessed from different contexts...
|
||||
.on('subPanelsUpdated', function(){
|
||||
// remove the panel when it runs out of sub-panels...
|
||||
if(!keep_empty && panel.find('.sub-panel:visible').length <= 0){
|
||||
removePanel(panel, true)
|
||||
}
|
||||
})
|
||||
.append((close_button
|
||||
? $('<summary>'+title+'</summary>')
|
||||
.append($('<span/>')
|
||||
.addClass('close-button')
|
||||
.click(function(){
|
||||
removePanel(panel)
|
||||
return false
|
||||
})
|
||||
.html('×'))
|
||||
: $('<summary>'+title+'</summary>'))
|
||||
// XXX also do this on enter...
|
||||
// XXX
|
||||
.click(function(){
|
||||
if(!panel.prop('open')){
|
||||
var evt = 'panelOpening'
|
||||
} else {
|
||||
var evt = 'panelClosing'
|
||||
}
|
||||
panel.trigger(evt, panel)
|
||||
panel.find('.sub-panel').each(function(){
|
||||
var sub_panel = $(this)
|
||||
if(sub_panel.prop('open')){
|
||||
sub_panel.trigger(evt, sub_panel)
|
||||
}
|
||||
})
|
||||
}))
|
||||
.draggable({
|
||||
containment: 'parent',
|
||||
scroll: false,
|
||||
stack: '.panel',
|
||||
// sanp to panels...
|
||||
//snap: ".panel",
|
||||
//snapMode: "outer",
|
||||
})
|
||||
.css({
|
||||
// NOTE: for some reason this is overwritten by jquery-ui to
|
||||
// 'relative' if it's not set explicitly...
|
||||
position: 'absolute',
|
||||
})
|
||||
|
||||
// content -- wrapper for sub-panels...
|
||||
var content = $('<span class="panel-content content">')
|
||||
.sortable({
|
||||
// general settings...
|
||||
forcePlaceholderSize: true,
|
||||
forceHelperSize: true,
|
||||
opacity: 0.7,
|
||||
connectWith: '.panel-content',
|
||||
|
||||
helper: _prepareHelper,
|
||||
start: _startSortHandler,
|
||||
|
||||
// - create a new panel when dropping outside of curent panel...
|
||||
// - remove empty panels...
|
||||
beforeStop: function(e, ui){
|
||||
// do this only when dropping outside the panel...
|
||||
if(ui.item.data('isoutside')
|
||||
// prevent draggingout the last panel...
|
||||
// NOTE: 2 because we are taking into account
|
||||
// the placeholders...
|
||||
&& panel.find('.sub-panel').length > 2){
|
||||
wrapWithPanel(ui.item, panel.parent(), ui.offset)
|
||||
}
|
||||
|
||||
panel.trigger('subPanelsUpdated')
|
||||
|
||||
_resetSidePanels()
|
||||
_resetSortedElem(ui.item)
|
||||
.data('isoutside', false)
|
||||
},
|
||||
|
||||
over: function(e, ui){
|
||||
ui.item.data('isoutside', false)
|
||||
ui.placeholder
|
||||
//.height(ui.helper.outerHeight())
|
||||
// NOTE: for some reason width does not allways get
|
||||
// set by jquery-ui...
|
||||
.width(ui.helper.outerWidth())
|
||||
.show()
|
||||
},
|
||||
out: function(e, ui){
|
||||
ui.item.data('isoutside', true)
|
||||
ui.placeholder.hide()
|
||||
},
|
||||
})
|
||||
.appendTo(panel)
|
||||
|
||||
if(parent != false){
|
||||
panel.appendTo(parent)
|
||||
}
|
||||
|
||||
// NOTE: no need to call the panelOpening event here as at this point
|
||||
// no one had the chance to bind a handler...
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
||||
// side can be:
|
||||
// - left
|
||||
// - right
|
||||
//
|
||||
// XXX in part this is exactly the same as makePanel
|
||||
// XXX need to trigger open/close sub panel events...
|
||||
function makeSidePanel(side, parent, autohide){
|
||||
autohide = autohide == null ? 'on' : 'off'
|
||||
parent = parent == null ? $(PANEL_ROOT) : parent
|
||||
var panel = $('.side-panel.'+side)
|
||||
// only one panel from each side can exist...
|
||||
if(panel.length != 0){
|
||||
return panel
|
||||
}
|
||||
panel = $('<div/>')
|
||||
.addClass('side-panel panel-content ' + side)
|
||||
.attr('autohide', autohide)
|
||||
// XXX trigger open/close events on hide/show..
|
||||
// XXX
|
||||
// toggle auto-hide...
|
||||
.dblclick(function(e){
|
||||
var elem = $(this)
|
||||
if(elem.attr('autohide') == 'off'){
|
||||
elem.attr('autohide', 'on')
|
||||
} else {
|
||||
elem.attr('autohide', 'off')
|
||||
}
|
||||
return false
|
||||
})
|
||||
// hide temporarily opened side-panels...
|
||||
.mouseout(function(){
|
||||
// XXX jQuery bug: this does not work...
|
||||
//panel.prop('open', false)
|
||||
panel.attr('open', null)
|
||||
})
|
||||
.sortable({
|
||||
forcePlaceholderSize: true,
|
||||
opacity: 0.7,
|
||||
connectWith: '.panel-content',
|
||||
|
||||
helper: _prepareHelper,
|
||||
start: _startSortHandler,
|
||||
|
||||
// - create a new panel when dropping outside of curent panel...
|
||||
// - remove empty panels...
|
||||
beforeStop: function(e, ui){
|
||||
// do this only when dropping outside the panel...
|
||||
if(ui.item.data('isoutside')){
|
||||
wrapWithPanel(ui.item, panel.parent(), ui.offset)
|
||||
}
|
||||
_resetSidePanels()
|
||||
_resetSortedElem(ui.item)
|
||||
.data('isoutside', false)
|
||||
},
|
||||
|
||||
over: function(e, ui){
|
||||
ui.item.data('isoutside', false)
|
||||
ui.placeholder
|
||||
//.height(ui.helper.outerHeight())
|
||||
// NOTE: for some reason width does not allways get
|
||||
// set by jquery-ui...
|
||||
.width(ui.helper.outerWidth())
|
||||
.show()
|
||||
},
|
||||
out: function(e, ui){
|
||||
ui.item.data('isoutside', true)
|
||||
ui.placeholder.hide()
|
||||
},
|
||||
})
|
||||
|
||||
if(parent != false){
|
||||
panel.appendTo(parent)
|
||||
}
|
||||
|
||||
// NOTE: no need to call the panelOpening event here as at this point
|
||||
// no one had the chance to bind a handler...
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
||||
// NOTE: if parent is not given this will create a new panel...
|
||||
// NOTE: title must be unique...
|
||||
function makeSubPanel(title, content, parent, open, content_resizable, close_button){
|
||||
title = title == null || title.trim() == '' ? ' ' : title
|
||||
parent = parent == null ? makePanel() : parent
|
||||
close_button = close_button == null ? true : close_button
|
||||
|
||||
open = open == null ? true : open
|
||||
content_resizable = content_resizable == null
|
||||
? false
|
||||
: content_resizable
|
||||
|
||||
var content_elem = $('<div class="sub-panel-content content"/>')
|
||||
if(content != null){
|
||||
content_elem
|
||||
.append(content)
|
||||
}
|
||||
var sub_panel = $('<details/>')
|
||||
.attr('id', title)
|
||||
.addClass('sub-panel noScroll')
|
||||
.prop('open', open)
|
||||
.append((close_button
|
||||
? $('<summary>'+title+'</summary>')
|
||||
.append($('<span/>')
|
||||
.addClass('close-button')
|
||||
.click(function(){
|
||||
var parent = sub_panel.parents('.panel').first()
|
||||
removePanel(sub_panel)
|
||||
// notify the parent context update...
|
||||
parent.trigger('subPanelsUpdated')
|
||||
return false
|
||||
})
|
||||
.html('×'))
|
||||
: $('<summary>'+title+'</summary>'))
|
||||
// XXX also do this on enter...
|
||||
// XXX
|
||||
.click(function(){
|
||||
if(!sub_panel.prop('open')){
|
||||
sub_panel.trigger('panelOpening', sub_panel)
|
||||
} else {
|
||||
sub_panel.trigger('panelClosing', sub_panel)
|
||||
}
|
||||
}))
|
||||
.append(content_elem)
|
||||
|
||||
if(parent != null && parent != false){
|
||||
if(parent.hasClass('panel-content')){
|
||||
sub_panel.appendTo(parent)
|
||||
} else {
|
||||
sub_panel.appendTo(parent.find('.panel-content'))
|
||||
}
|
||||
}
|
||||
|
||||
if(content_resizable){
|
||||
// NOTE: we are wrapping the content into a div so as to make
|
||||
// the fact that the panel is resizable completely
|
||||
// transparent for the user -- no need to be aware of the
|
||||
// sizing elements, etc.
|
||||
content_elem.wrap($('<div>')).parent()
|
||||
.resizable({
|
||||
handles: 's',
|
||||
})
|
||||
.css({
|
||||
overflow: 'hidden',
|
||||
})
|
||||
}
|
||||
|
||||
// NOTE: no need to call the panelOpening event here as at this point
|
||||
// no one had the chance to bind a handler...
|
||||
|
||||
return sub_panel
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Actions...
|
||||
*/
|
||||
|
||||
// XXX this should take the state into consideration while opening panels
|
||||
// and open panels in specific parents and locations, maybe even with
|
||||
// other neighbor panels...
|
||||
// XXX currently parent is ignored if panel is already created, is this
|
||||
// correct???
|
||||
// XXX update panel state...
|
||||
function openPanel(panel, parent, no_blink){
|
||||
var title = typeof(panel) == typeof('str') ? panel : null
|
||||
panel = typeof(panel) == typeof('str')
|
||||
? getPanel(panel)
|
||||
: panel
|
||||
title = title == null ? panel.attr('id') : title
|
||||
var open = false
|
||||
|
||||
// create a new panel...
|
||||
if(panel.length == 0){
|
||||
if(title in PANELS){
|
||||
var builder = PANELS[title]
|
||||
panel = builder({
|
||||
open: true,
|
||||
parent: parent,
|
||||
})
|
||||
}
|
||||
|
||||
// show/open the panel and all it's parents...
|
||||
} else {
|
||||
open = isPanelVisible(panel)
|
||||
// show panels...
|
||||
panel
|
||||
.css('display', '')
|
||||
.prop('open', true)
|
||||
.parents('.panel')
|
||||
.css('display', '')
|
||||
.prop('open', true)
|
||||
// show side panels...
|
||||
panel
|
||||
.parents('.side-panel').first()
|
||||
// XXX jQuery bug: this does not work...
|
||||
//.prop('open', true)
|
||||
.attr('open', 'yes')
|
||||
}
|
||||
|
||||
// if the panel was not open trigger the event...
|
||||
if(!open){
|
||||
panel.trigger('panelOpening', panel)
|
||||
}
|
||||
|
||||
return no_blink ? panel : blinkPanel(panel)
|
||||
}
|
||||
|
||||
|
||||
// Open a set of sub-panels in one parent panel...
|
||||
//
|
||||
// returns the parent panel.
|
||||
//
|
||||
// NOTE: if parent is given and already exists then this will append the
|
||||
// new panels to it...
|
||||
// NOTE: this will not re-group already opened panels...
|
||||
function openGroupedPanels(panels, parent){
|
||||
panels = typeof(panels) == typeof('str') ? [panels] : panels
|
||||
parent = parent == null ? makePanel() : parent
|
||||
|
||||
panels.forEach(function(title){
|
||||
openPanel(title, parent, true)
|
||||
})
|
||||
|
||||
return parent
|
||||
}
|
||||
|
||||
|
||||
// XXX
|
||||
// XXX update panel state...
|
||||
function openPanels(){
|
||||
// XXX
|
||||
}
|
||||
|
||||
|
||||
// Close the panel...
|
||||
//
|
||||
// NOTE: this does not care if it's a panel or sub-panel...
|
||||
// XXX do we need a panelRemoved event???
|
||||
// ...and a symmetrical panelCreated??
|
||||
// XXX update panel state...
|
||||
function closePanel(panel){
|
||||
panel = typeof(panel) == typeof('str')
|
||||
? getPanel(panel)
|
||||
: panel
|
||||
panel.find('.sub-panel:visible').each(function(){
|
||||
var p = $(this)
|
||||
if(p.prop('open')){
|
||||
p.trigger('panelClosing', p)
|
||||
}
|
||||
})
|
||||
return panel
|
||||
.prop('open', false)
|
||||
.trigger('panelClosing', panel)
|
||||
}
|
||||
|
||||
|
||||
// Remove the panel after firing close events on it and all sub-panels...
|
||||
//
|
||||
// XXX update panel state...
|
||||
function removePanel(panel){
|
||||
panel = typeof(panel) == typeof('str')
|
||||
? getPanel(panel)
|
||||
: panel
|
||||
/*
|
||||
if(PANEL_CLOSE_METHOD == 'hide'){
|
||||
return closePanel(panel)
|
||||
.hide()
|
||||
} else {
|
||||
return closePanel(panel)
|
||||
.remove()
|
||||
}
|
||||
*/
|
||||
return closePanel(panel)
|
||||
.hide()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* High level interface...
|
||||
*/
|
||||
|
||||
//
|
||||
// content_builder() - should build and setup panel content
|
||||
// panel_setup(panel) - should register panel open/close event
|
||||
// handlers
|
||||
//
|
||||
// NOTE: this will search an element by title, so if it is not unique
|
||||
// an existing element will be returned...
|
||||
function Panel(title, content_builder, panel_setup, content_resizable){
|
||||
|
||||
var controller = function(state){
|
||||
state = state == null ? {} : state
|
||||
var parent = state.parent
|
||||
var open = state.open
|
||||
|
||||
// 1) search for panel and return it if it exists...
|
||||
var panel = getPanel(title)
|
||||
|
||||
// 2) if no panel exists, create it
|
||||
// - content_builder() must return panel content
|
||||
if(panel.length == 0){
|
||||
panel = makeSubPanel(title, content_builder(), parent, open, content_resizable)
|
||||
.attr('id', title)
|
||||
|
||||
panel_setup(panel)
|
||||
|
||||
// trigger the open event...
|
||||
if(isPanelVisible(panel)){
|
||||
panel.trigger('panelOpening', panel)
|
||||
}
|
||||
|
||||
} else {
|
||||
var v = isPanelVisible(panel)
|
||||
|
||||
if(open && !v){
|
||||
openPanel(panel)
|
||||
|
||||
} else if(!open && v){
|
||||
closePanel(panel)
|
||||
}
|
||||
}
|
||||
|
||||
// XXX set panel position, size, ...
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
PANELS[title] = controller
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
|
||||
// XXX also need:
|
||||
// - togglePanels()
|
||||
// show/hide all the panels (a-la Photoshop's Tab action)
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
function getPanelState(){
|
||||
var res = []
|
||||
|
||||
var _getPanel = function(){
|
||||
var panel = $(this)
|
||||
var offset = panel.offset()
|
||||
var sub_panels = panel.find('.sub-panel')
|
||||
|
||||
res.push({
|
||||
type: (panel.hasClass('panel') ? 'panel'
|
||||
: panel.hasClass('side-panel')
|
||||
&& panel.hasClass('left') ? 'side-panel-left'
|
||||
: panel.hasClass('side-panel')
|
||||
&& panel.hasClass('right') ? 'side-panel-right'
|
||||
: null),
|
||||
|
||||
top: offset.top,
|
||||
left: offset.left,
|
||||
|
||||
open: panel.prop('open') ? true : false,
|
||||
autohide: panel.attr('autohide'),
|
||||
|
||||
content: sub_panels.map(function(){
|
||||
var p = $(this)
|
||||
return {
|
||||
title: p.find('summary').text(),
|
||||
}
|
||||
}).toArray(),
|
||||
})
|
||||
}
|
||||
|
||||
$('.panel, .side-panel').each(_getPanel)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
function setPanelState(data){
|
||||
// XXX
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
521
ui (gen4)/lib/scroller.js
Executable file
521
ui (gen4)/lib/scroller.js
Executable file
@ -0,0 +1,521 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
* General Swipe/Scroll handler lib
|
||||
*
|
||||
* TODO a demo page to show/test the features...
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
// click threshold in pixels, if the distance between start and end is
|
||||
// less than this, the whole event is considered a click and not a
|
||||
// drag/swipe...
|
||||
var CLICK_THRESHOLD = 10
|
||||
|
||||
// if the amount of time to wait bteween start and end is greater than
|
||||
// this the event is considered a long click.
|
||||
// NOTE: this will not auto-fire the event, the user MUST release first.
|
||||
var LONG_CLICK_THRESHOLD = 400
|
||||
|
||||
// the maximum amount of time between clicks to count them together.
|
||||
// NOTE: if multi-clicks are disabled this has no effect.
|
||||
// NOTE: this is reset by the timeout explicitly set in the handler...
|
||||
// NOTE: this is the timeout between two consecutive clicks and not the
|
||||
// total.
|
||||
// NOTE: if multiple clicks are enabled this will introduce a lag after
|
||||
// each click (while we wait for the next), so keep this as small
|
||||
// as possible, but not too small as to rush the user too much.
|
||||
var MULTI_CLICK_TIMEOUT = 200
|
||||
|
||||
// the amount of time between finger releases.
|
||||
// NOTE: when this is passed all the fingers released before are ignored.
|
||||
var MULTITOUCH_RELEASE_THRESHOLD = 100
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// Scroll handler
|
||||
//
|
||||
// This will take two elements a root (container) and a scrolled (first
|
||||
// child of the container) and implement drag-scrolling of the scrolled
|
||||
// within the root.
|
||||
//
|
||||
// This calls the following callbacks if they are defined.
|
||||
// - preCallback (unset)
|
||||
// - scrollCallback (unset)
|
||||
// - postCallback (set to postScrollCallback)
|
||||
//
|
||||
// See scroller.options for configuration.
|
||||
//
|
||||
//
|
||||
// XXX add a resonable cancel scheme...
|
||||
// ... something similar to touch threshold but bigger...
|
||||
// XXX setup basic styles for the contained element...
|
||||
// XXX revise...
|
||||
// XXX test on other devices...
|
||||
function makeScrollHandler(root, config){
|
||||
root = $(root)
|
||||
|
||||
// local data...
|
||||
var ignoring = false
|
||||
// XXX this and scroller.state are redundent...
|
||||
var scrolling = false
|
||||
var touch = false
|
||||
var touches = 0
|
||||
var max_dx = 0
|
||||
var max_dy = 0
|
||||
|
||||
var cancelThreshold, scrolled
|
||||
// initial state...
|
||||
, start_x, start_y, start_t
|
||||
// previous state...
|
||||
, prev_x, prev_y, prev_t
|
||||
// current state...
|
||||
, x, y, t
|
||||
// state delta...
|
||||
, dx, dy, dt
|
||||
|
||||
, shift
|
||||
, scale
|
||||
//, bounds
|
||||
|
||||
function startMoveHandler(evt){
|
||||
var options = scroller.options
|
||||
// ignore...
|
||||
if(options.ignoreElements
|
||||
&& $(evt.target).closest(options.ignoreElements).length > 0
|
||||
|| scroller.state == 'paused'){
|
||||
ignoring = true
|
||||
return
|
||||
} else {
|
||||
ignoring = false
|
||||
}
|
||||
if(event.touches != null){
|
||||
touch = true
|
||||
}
|
||||
cancelThreshold = options.scrollCancelThreshold
|
||||
touches = touch ? event.touches.length : 1
|
||||
// if we are already touching then just skip on this...
|
||||
// XXX test this...
|
||||
if(touches > 1){
|
||||
return false
|
||||
}
|
||||
prev_t = event.timeStamp || Date.now();
|
||||
start_t = prev_t
|
||||
/*
|
||||
if(options.autoCancelEvents){
|
||||
bounds = {
|
||||
left: options.eventBounds,
|
||||
right: root.width() - options.eventBounds,
|
||||
top: options.eventBounds,
|
||||
bottom: root.height() - options.eventBounds
|
||||
}
|
||||
}
|
||||
*/
|
||||
//scrolled = $(root.children()[0])
|
||||
scrolled = root.children().first()
|
||||
setTransitionDuration(scrolled, 0)
|
||||
// XXX these two are redundant...
|
||||
scrolling = true
|
||||
scroller.state = 'scrolling'
|
||||
// XXX do we need to pass something to this?
|
||||
options.preCallback && options.preCallback()
|
||||
|
||||
shift = getElementShift(scrolled)
|
||||
scale = getElementScale(scrolled)
|
||||
// get the user coords...
|
||||
prev_x = touch ? event.touches[0].pageX : evt.clientX
|
||||
start_x = prev_x
|
||||
prev_y = touch ? event.touches[0].pageY : evt.clientY
|
||||
start_y = prev_y
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// XXX try and make this adaptive to stay ahead of the lags...
|
||||
// NOTE: this does not support limiting the scroll, might be done in
|
||||
// the future though.
|
||||
// The way to go about this is to track scrolled size in the
|
||||
// callback...
|
||||
function moveHandler(evt){
|
||||
if(ignoring){
|
||||
return
|
||||
}
|
||||
var options = scroller.options
|
||||
evt.preventDefault()
|
||||
t = event.timeStamp || Date.now();
|
||||
// get the user coords...
|
||||
x = touch ? event.touches[0].pageX : evt.clientX
|
||||
y = touch ? event.touches[0].pageY : evt.clientY
|
||||
touches = touch ? event.touches.length : 1
|
||||
|
||||
/*
|
||||
// XXX needs testing...
|
||||
// XXX do we need to account for scrollDisabled here???
|
||||
// check scroll bounds...
|
||||
if(bounds != null){
|
||||
if(options.hScroll && (x <= bounds.left || x >= bounds.right)
|
||||
|| options.vScroll && (y <= bounds.top || y >= bounds.bottom)){
|
||||
// XXX cancel the touch event and trigger the end handler...
|
||||
return endMoveHandler(evt)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// do the actual scroll...
|
||||
if(!options.scrollDisabled && scrolling){
|
||||
if(options.hScroll){
|
||||
shift.left += x - prev_x
|
||||
}
|
||||
if(options.vScroll){
|
||||
shift.top += y - prev_y
|
||||
}
|
||||
setElementTransform(scrolled, shift, scale)
|
||||
|
||||
// XXX these should be done every time the event is caught or
|
||||
// just while scrolling?
|
||||
dx = x - prev_x
|
||||
dy = y - prev_y
|
||||
max_dx += Math.abs(dx)
|
||||
max_dy += Math.abs(dy)
|
||||
dt = t - prev_t
|
||||
prev_x = x
|
||||
prev_y = y
|
||||
prev_t = t
|
||||
|
||||
// XXX do we need to pass something to this?
|
||||
options.scrollCallback && options.scrollCallback()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function endMoveHandler(evt){
|
||||
t = event.timeStamp || Date.now();
|
||||
touches = touch ? event.touches.length : 0
|
||||
if(ignoring){
|
||||
if(touches == 0){
|
||||
ignoring = false
|
||||
}
|
||||
return
|
||||
}
|
||||
var options = scroller.options
|
||||
|
||||
// XXX get real transition duration...
|
||||
scroller.resetTransitions()
|
||||
|
||||
x = touch ? event.changedTouches[0].pageX : evt.clientX
|
||||
y = touch ? event.changedTouches[0].pageY : evt.clientY
|
||||
// check if we are canceling...
|
||||
if(cancelThreshold
|
||||
&& Math.abs(start_x-x) < cancelThreshold
|
||||
&& Math.abs(start_y-y) < cancelThreshold
|
||||
&& (max_dx > cancelThreshold
|
||||
|| max_dy > cancelThreshold)){
|
||||
scroller.state = 'canceling'
|
||||
}
|
||||
|
||||
var data = {
|
||||
orig_event: evt,
|
||||
scroller: scroller,
|
||||
speed: {
|
||||
x: dx/dt,
|
||||
y: dy/dt
|
||||
},
|
||||
distance: {
|
||||
x: start_x-x,
|
||||
y: start_y-y
|
||||
},
|
||||
duration: t-start_t,
|
||||
// current touches...
|
||||
touches: touches,
|
||||
clicks: null,
|
||||
}
|
||||
// XXX stop only if no fingers are touching or let the callback decide...
|
||||
if(options.postCallback
|
||||
// XXX revise this....
|
||||
&& options.postCallback(data) === false
|
||||
|| touches == 0){
|
||||
// cleanup and stop...
|
||||
touch = false
|
||||
scrolling = false
|
||||
scroller.state = 'waiting'
|
||||
scrolled = null
|
||||
//bounds = null
|
||||
max_dx = 0
|
||||
max_dy = 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
var scroller = {
|
||||
options: {
|
||||
// if one of these is false, it will restrict scrolling in
|
||||
// that direction. hScroll for horizontal and vScroll for
|
||||
// vertical.
|
||||
// NOTE: to disable scroll completely use scrollDisabled, see
|
||||
// below for details.
|
||||
hScroll: true,
|
||||
vScroll: true,
|
||||
|
||||
// this will disable scroll.
|
||||
// NOTE: this is the same as setting both vScroll and hScroll
|
||||
// to false, but can be set and reset without affecting
|
||||
// the actual settings individually...
|
||||
// NOTE: this takes priority over hScroll/vScroll.
|
||||
scrollDisabled: false,
|
||||
|
||||
// sets the default transition settings while not scrolling...
|
||||
transitionDuration: 200,
|
||||
transitionEasing: 'ease',
|
||||
|
||||
// items to be ignored by the scroller...
|
||||
// this is a jQuery compatible selector.
|
||||
ignoreElements: '.noScroll',
|
||||
// this is the side of the rectangle in px, if the user moves
|
||||
// out of it, and then returns back, the action will get cancelled.
|
||||
// i.e. the callback will get called with the "cancelling" state.
|
||||
scrollCancelThreshold: 100,
|
||||
|
||||
/*
|
||||
// XXX padding within the target element moving out of which
|
||||
// will cancell the action...
|
||||
// XXX needs testing...
|
||||
autoCancelEvents: false,
|
||||
eventBounds: 5,
|
||||
*/
|
||||
|
||||
// callback to be called when the user first touches the screen...
|
||||
preCallback: null,
|
||||
// callback to be called when a scroll step is done...
|
||||
scrollCallback: null,
|
||||
// callback to be called when the user lifts a finger/mouse.
|
||||
// NOTE: this may happen before the scroll is done, for instance
|
||||
// when one of several fingers participating in the action
|
||||
// gets lifted.
|
||||
// NOTE: if this returns false explicitly, this will stop scrolling.
|
||||
postCallback: postScrollCallback,
|
||||
|
||||
// These are used by the default callback...
|
||||
//
|
||||
// if true then doubleClick and multiClick events will get
|
||||
// triggered.
|
||||
// NOTE: this will introduce a lag needed to wait for next
|
||||
// clicks in a group.
|
||||
// NOTE: when this is false, shortClick is triggered for every
|
||||
// single click separately.
|
||||
enableMultiClicks: false,
|
||||
// NOTE: if these are null, respective values from the env will
|
||||
// be used.
|
||||
clickThreshold: null,
|
||||
longClickThreshold: null,
|
||||
multiClickTimeout: null,
|
||||
multitouchTimeout: null,
|
||||
},
|
||||
state: 'stopped',
|
||||
root: root,
|
||||
|
||||
start: function(){
|
||||
if(this.state == 'paused'){
|
||||
this.state = 'waiting'
|
||||
} else {
|
||||
this.state = 'waiting'
|
||||
|
||||
// NOTE: if we bind both touch and mouse events, on touch devices they
|
||||
// might start interfering with each other...
|
||||
if('ontouchmove' in window){
|
||||
root
|
||||
.on('touchstart', startMoveHandler)
|
||||
.on('touchmove', moveHandler)
|
||||
.on('touchend', endMoveHandler)
|
||||
.on('touchcancel', endMoveHandler)
|
||||
} else {
|
||||
root
|
||||
.on('mousedown', startMoveHandler)
|
||||
.on('mousemove', moveHandler)
|
||||
.on('mouseup', endMoveHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// setup transitions...
|
||||
this.resetTransitions()
|
||||
|
||||
return this
|
||||
},
|
||||
// XXX test...
|
||||
pause: function(){
|
||||
this.state = 'paused'
|
||||
return this
|
||||
},
|
||||
stop: function(){
|
||||
if('ontouchmove' in window){
|
||||
root
|
||||
.off('touchstart', startMoveHandler)
|
||||
.off('touchmove', moveHandler)
|
||||
.off('touchend', endMoveHandler)
|
||||
.off('touchcancel', endMoveHandler)
|
||||
} else {
|
||||
root
|
||||
.off('mousedown', startMoveHandler)
|
||||
.off('mousemove', moveHandler)
|
||||
.off('mouseup', endMoveHandler)
|
||||
}
|
||||
this.state = 'stopped'
|
||||
return this
|
||||
},
|
||||
resetTransitions: function(){
|
||||
var scrolled = this.root.children().first()
|
||||
setTransitionDuration(scrolled, this.options.transitionDuration)
|
||||
setTransitionEasing(scrolled, this.options.transitionEasing)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// merge the config with the defaults...
|
||||
if(config != null){
|
||||
$.extend(scroller.options, config)
|
||||
}
|
||||
|
||||
return scroller
|
||||
}
|
||||
|
||||
|
||||
|
||||
// default callback...
|
||||
//
|
||||
// This will provide support for the following events on the scroll root
|
||||
// element:
|
||||
// - scrollCancelled
|
||||
//
|
||||
// - shortClick
|
||||
// - doubleClick
|
||||
// - multiClick
|
||||
// this will store the number of clicks in data.clicks
|
||||
// - longClick
|
||||
//
|
||||
// - swipeLeft
|
||||
// - swipeRight
|
||||
// - swipeUp
|
||||
// - swipeDown
|
||||
//
|
||||
// - screenReleased
|
||||
//
|
||||
// NOTE: data.touches passed to the event is the number of touches
|
||||
// released within the multitouchTimeout.
|
||||
// this differs from what postScrollCallback actually gets in the
|
||||
// same field when it receives the scroll data object.
|
||||
// XXX add generic snap
|
||||
// XXX add generic inertial scroll
|
||||
// ...see jli.js/animateElementTo for a rough implementation
|
||||
// XXX test multiple touches...
|
||||
function postScrollCallback(data){
|
||||
var scroller = data.scroller
|
||||
var options = scroller.options
|
||||
var root = scroller.root
|
||||
var clickThreshold = options.clickThreshold || CLICK_THRESHOLD
|
||||
var longClickThreshold = options.longClickThreshold || LONG_CLICK_THRESHOLD
|
||||
var multitouchTimeout = options.multitouchTimeout || MULTITOUCH_RELEASE_THRESHOLD
|
||||
var enableMultiClicks = options.enableMultiClicks
|
||||
var multiClickTimeout = options.multiClickTimeout || MULTI_CLICK_TIMEOUT
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
// cancel event...
|
||||
if(scroller.state == 'canceling'){
|
||||
return root.trigger('scrollCancelled', data)
|
||||
}
|
||||
|
||||
// handle multiple touches...
|
||||
if(data.touches > 0){
|
||||
var then = scroller._last_touch_release
|
||||
if(then == null || now - then < multitouchTimeout){
|
||||
if(scroller._touches == null){
|
||||
scroller._touches = 1
|
||||
} else {
|
||||
scroller._touches += 1
|
||||
}
|
||||
} else {
|
||||
scroller._touches = null
|
||||
}
|
||||
// wait for the next touch release...
|
||||
scroller._last_touch_release = now
|
||||
return
|
||||
|
||||
// calculate how many touches did participate...
|
||||
} else {
|
||||
data.touches = scroller._touches ? scroller._touches + 1 : 1
|
||||
scroller._last_touch_release = null
|
||||
scroller._touches = null
|
||||
}
|
||||
|
||||
// clicks, double-clicks, multi-clicks and long-clicks...
|
||||
if(Math.max(
|
||||
Math.abs(data.distance.x),
|
||||
Math.abs(data.distance.y)) < clickThreshold){
|
||||
if(data.duration > longClickThreshold){
|
||||
return root.trigger('longClick', data)
|
||||
}
|
||||
if(!enableMultiClicks){
|
||||
return root.trigger('shortClick', data)
|
||||
|
||||
} else {
|
||||
// count the clicks so far...
|
||||
if(scroller._clicks == null){
|
||||
scroller._clicks = 1
|
||||
} else {
|
||||
scroller._clicks += 1
|
||||
}
|
||||
|
||||
// kill any previous waits...
|
||||
if(scroller._click_timeout_id != null){
|
||||
clearTimeout(scroller._click_timeout_id)
|
||||
}
|
||||
|
||||
// wait for the next click...
|
||||
scroller._click_timeout_id = setTimeout(function(){
|
||||
var clicks = scroller._clicks
|
||||
data.clicks = clicks
|
||||
if(clicks == 1){
|
||||
root.trigger('shortClick', data)
|
||||
} else if(clicks == 2){
|
||||
root.trigger('doubleClick', data)
|
||||
} else {
|
||||
root.trigger('multiClick', data)
|
||||
}
|
||||
scroller._clicks = null
|
||||
scroller._click_timeout_id = null
|
||||
}, multiClickTimeout)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// swipes...
|
||||
// XXX might be a good idea to chain these with swipe and screenReleased
|
||||
if(Math.abs(data.distance.x) > Math.abs(data.distance.y)){
|
||||
if(data.distance.x <= -clickThreshold && root.data('events').swipeLeft){
|
||||
return root.trigger('swipeLeft', data)
|
||||
} else if(data.distance.x >= clickThreshold && root.data('events').swipeRight){
|
||||
return root.trigger('swipeRight', data)
|
||||
}
|
||||
} else {
|
||||
if(data.distance.y <= -clickThreshold && root.data('events').swipeUp){
|
||||
return root.trigger('swipeUp', data)
|
||||
} else if(data.distance.y >= clickThreshold && root.data('events').swipeDown){
|
||||
return root.trigger('swipeDown', data)
|
||||
}
|
||||
}
|
||||
|
||||
// this is triggered if no swipes were handled...
|
||||
return root.trigger('screenReleased', data)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
16
ui (gen4)/loader.js
Executable file
16
ui (gen4)/loader.js
Executable file
@ -0,0 +1,16 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
16
ui (gen4)/package.json
Executable file
16
ui (gen4)/package.json
Executable file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ImageGrid.Viewer",
|
||||
"main": "index.html",
|
||||
"version": "0.0.1",
|
||||
|
||||
"window": {
|
||||
"title": "ImageGrid.Viewer",
|
||||
"position": "center",
|
||||
"width": 900,
|
||||
"height": 700,
|
||||
"min_width": 400,
|
||||
"min_height": 400,
|
||||
"frame": false,
|
||||
"toolbar": false
|
||||
}
|
||||
}
|
||||
351
ui (gen4)/ribbons.js
Executable file
351
ui (gen4)/ribbons.js
Executable file
@ -0,0 +1,351 @@
|
||||
/**********************************************************************
|
||||
*
|
||||
* Minomal UI API...
|
||||
*
|
||||
*
|
||||
**********************************************************************/
|
||||
|
||||
//var DEBUG = DEBUG != null ? DEBUG : true
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
var RibbonsClassPrototype = {
|
||||
}
|
||||
|
||||
|
||||
// XXX this is a low level interface, not a set of actions...
|
||||
// XXX test
|
||||
var RibbonsPrototype = {
|
||||
//
|
||||
// .viewer (jQuery object)
|
||||
//
|
||||
|
||||
// NOTE: these accept gids or jQuery objects...
|
||||
getRibbon: function(target){
|
||||
if(target == null) {
|
||||
return this.viewer.find('.current.image').parents('.ribbon').first()
|
||||
|
||||
} else if(typeof(target) == typeof('str')){
|
||||
return this.viewer.find('.ribbon[gid='+JSON.stringify(target)+']')
|
||||
}
|
||||
return $(target).filter('.ribbon')
|
||||
},
|
||||
getImage: function(target){
|
||||
if(target == null) {
|
||||
return this.viewer.find('.current.image')
|
||||
|
||||
} else if(typeof(target) == typeof('str')){
|
||||
return this.viewer.find('.image[gid='+JSON.stringify(target)+']')
|
||||
}
|
||||
return $(target).filter('.image')
|
||||
},
|
||||
|
||||
// NOTE: these will return unattached objects...
|
||||
createRibbon: function(gid){
|
||||
return $('<div>')
|
||||
.addClass('ribbon')
|
||||
.setAttribute('gid', JSON.stringify(gid))
|
||||
},
|
||||
createImage: function(gid){
|
||||
return $('<div>')
|
||||
.addClass('image')
|
||||
.setAttribute('gid', JSON.stringify(gid))
|
||||
},
|
||||
|
||||
|
||||
// NOTE: to remove a ribbon or an image just use .getRibbon(..).remove()
|
||||
// and .getImage(...).remove() respectivly.
|
||||
|
||||
|
||||
// Place a ribbon...
|
||||
//
|
||||
// position can be:
|
||||
// - index
|
||||
// - ribbon gid
|
||||
// - ribbon
|
||||
//
|
||||
// NOTE: if ribbon does not exist a new ribbon will be created...
|
||||
// XXX these will place at current loaded position rather than the
|
||||
// actual DATA position...
|
||||
// ...is this correct?
|
||||
// XXX interaction animation...
|
||||
placeRibbon: function(gid, position){
|
||||
// get create the ribbon...
|
||||
var ribbon = this.getRibbon(gid)
|
||||
ribbon = ribbon.length == 0 ? this.createRibbon(gid) : ribbon
|
||||
|
||||
var ribbons = this.viewer.find('.ribbon')
|
||||
// normalize the position...
|
||||
var p = this.getRibbon(position)
|
||||
position = p.hasClass('ribbon') ? ribbons.index(p) : position
|
||||
position = position < 0 ? (ribbons.length - position)+1 : position
|
||||
position = position < 0 ? 0 : position
|
||||
|
||||
// place the ribbon...
|
||||
if(ribbons.length <= position){
|
||||
ribbons.last().after(ribbon)
|
||||
} else {
|
||||
ribbons.eq(position).before(ribbon)
|
||||
}
|
||||
|
||||
// XXX do we need to update the ribbon here???
|
||||
return ribbon
|
||||
},
|
||||
|
||||
// Place an image...
|
||||
//
|
||||
// Place gid at image position and image ribbon:
|
||||
// .placeImage(gid, image)
|
||||
// -> image
|
||||
//
|
||||
// Place gid at index in current ribbon:
|
||||
// .placeImage(gid, position)
|
||||
// -> image
|
||||
//
|
||||
// Place gid at position in ribbon:
|
||||
// .placeImage(gid, ribbon, position)
|
||||
// -> image
|
||||
//
|
||||
//
|
||||
// NOTE: if image gid does not exist it will be created.
|
||||
// NOTE: index can be negative indicating the position from the tail.
|
||||
// NOTE: if index is an image or a gid then the ribbon argument will
|
||||
// be ignored and the actual ribbon will be derived from the
|
||||
// image given.
|
||||
// XXX interaction animation...
|
||||
placeImage: function(gid, ribbon, position){
|
||||
// get/create the image...
|
||||
var image = this.getImage(gid)
|
||||
image = image.length == 0 ? this.createImage(gid) : image
|
||||
|
||||
// normalize the position, ribbon and images...
|
||||
if(position == null){
|
||||
position = ribbon
|
||||
ribbon = null
|
||||
}
|
||||
var p = this.getImage(position)
|
||||
ribbon = p.hasClass('image')
|
||||
? p.parents('.ribbon').first()
|
||||
: this.getRibbon(ribbon)
|
||||
var images = ribbon.find('.image')
|
||||
position = p.hasClass('image') ? images.index(p) : position
|
||||
position = position < 0 ? (images.length - position)+1 : position
|
||||
position = position < 0 ? 0 : position
|
||||
|
||||
// place the image...
|
||||
if(images.length <= position){
|
||||
ribbon.append(image)
|
||||
} else {
|
||||
images.eq(position).before(image)
|
||||
}
|
||||
|
||||
return updateImage(image)
|
||||
},
|
||||
|
||||
// XXX this does not align anything, it's just a low level focus...
|
||||
// XXX interaction animation...
|
||||
focusImage: function(gid){
|
||||
this.viewer
|
||||
.find('.current.image')
|
||||
.removeClass('current')
|
||||
return this.getImage(gid)
|
||||
.addClass('current')
|
||||
},
|
||||
|
||||
|
||||
// Image manipulation...
|
||||
|
||||
// Rotate an image...
|
||||
//
|
||||
// direction can be:
|
||||
// XXX not sure if we need these as attrs...
|
||||
CW: 'cw',
|
||||
CCW: 'ccw',
|
||||
//
|
||||
// rotation tables...
|
||||
// NOTE: setting a value to null will remove the attribute, 0 will
|
||||
// set 0 explicitly...
|
||||
_cw: {
|
||||
null: 90,
|
||||
0: 90,
|
||||
90: 180,
|
||||
180: 270,
|
||||
//270: 0,
|
||||
270: null,
|
||||
},
|
||||
_ccw: {
|
||||
null: 270,
|
||||
0: 270,
|
||||
//90: 0,
|
||||
90: null,
|
||||
180: 90,
|
||||
270: 180,
|
||||
},
|
||||
rotateImage: function(target, direction){
|
||||
var r_table = direction == this.CW ? _cw : _ccw
|
||||
target = this.getImage(target)
|
||||
target.each(function(i, e){
|
||||
var img = $(this)
|
||||
var o = r_table[img.attr('orientation')]
|
||||
if(o == null){
|
||||
img.removeAttr('orientation')
|
||||
} else {
|
||||
img.attr('orientation', o)
|
||||
}
|
||||
// account for proportions...
|
||||
correctImageProportionsForRotation(img)
|
||||
// XXX this is a bit of an overkill but it will update the
|
||||
// preview if needed...
|
||||
//updateImage(img)
|
||||
})
|
||||
return target
|
||||
},
|
||||
|
||||
// Flip an image...
|
||||
//
|
||||
// direction can be:
|
||||
// XXX not sure if we need these as attrs...
|
||||
VERTICAL: 'vertical',
|
||||
HORIZONTAL: 'horizontal',
|
||||
flipImage: function(target, direction){
|
||||
target = this.getImage(target)
|
||||
target.each(function(i, e){
|
||||
var img = $(this)
|
||||
|
||||
// get the state...
|
||||
var state = img.attr('flipped')
|
||||
state = (state == null ? '' : state)
|
||||
.split(',')
|
||||
.map(function(e){ return e.trim() })
|
||||
.filter(function(e){ return e != '' })
|
||||
|
||||
// toggle the specific state...
|
||||
var i = state.indexOf(direction)
|
||||
if(i >= 0){
|
||||
state.splice(i, 1)
|
||||
} else {
|
||||
state.push(direction)
|
||||
}
|
||||
|
||||
// write the state...
|
||||
if(state.length == 0){
|
||||
img.removeAttr('flipped')
|
||||
} else {
|
||||
img.attr('flipped', state.join(', '))
|
||||
}
|
||||
})
|
||||
return target
|
||||
},
|
||||
|
||||
// shorthands...
|
||||
// XXX should these be here???
|
||||
rotateCW: function(target){ return this.rotateImage(target, this.CW) },
|
||||
rotateCCW: function(target){ return this.rotateImage(target, this.CCW) },
|
||||
flipVertical: function(target){ return this.flipImage(target, this.VERTICAL) },
|
||||
flipHorizontal: function(target){ return this.flipImage(target, this.HORIZONTAL) },
|
||||
|
||||
|
||||
// Bulk manipulation...
|
||||
|
||||
// NOTE: gids and ribbon must be .getImage(..) and .getRibbon(..)
|
||||
// compatible...
|
||||
// XXX do we need an image pool here???
|
||||
showImagesInRibbon: function(gids, ribbon){
|
||||
// get/create the ribbon...
|
||||
var r = this.getRibbon(ribbon)
|
||||
if(r.length == 0){
|
||||
r = this.createRibbon(ribbon)
|
||||
}
|
||||
|
||||
var loaded = r.find('.image')
|
||||
|
||||
var that = this
|
||||
$(gids).each(function(gid, i){
|
||||
// get/create image...
|
||||
var img = that.getImage(gid)
|
||||
if(img.length == 0){
|
||||
img = that.createImage(gid)
|
||||
}
|
||||
|
||||
// clear images that are not in gids...
|
||||
var g = JSON.parse(loaded.eq(i).attr('gid'))
|
||||
while(gids.indexOf(g) < 0){
|
||||
//r.find('[gid='+JSON.stringify(g)+']')
|
||||
// .remove()
|
||||
that.clear(g)
|
||||
loaded.splice(i, 1)
|
||||
g = JSON.parse(loaded.eq(i).attr('gid'))
|
||||
}
|
||||
|
||||
// check if we need to reattach the image...
|
||||
if(gid != g){
|
||||
// attach the image at i...
|
||||
loaded.eq(i).before(img.detach())
|
||||
}
|
||||
|
||||
updateImage(img)
|
||||
})
|
||||
|
||||
// remove the rest of the stuff in ribbon...
|
||||
if(loaded.length > gids.length){
|
||||
loaded.eq(gids.length).nextAll().remove()
|
||||
loaded.eq(gids.length).remove()
|
||||
}
|
||||
|
||||
return this
|
||||
},
|
||||
clear: function(gids){
|
||||
// clear all...
|
||||
if(gids == null){
|
||||
this.viewer.find('.ribbon').remove()
|
||||
|
||||
// clear one or more gids...
|
||||
} else {
|
||||
gids = gids.constructor.name != 'Array' ? [gids] : gids
|
||||
var that = this
|
||||
gids.forEach(function(g){
|
||||
that.viewer.find('[gid='+JSON.stringify(g)+']').remove()
|
||||
})
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
|
||||
// UI manipulation...
|
||||
|
||||
// XXX if target is an image align the ribbon both vertically and horizontally...
|
||||
alignRibbon: function(target, mode){
|
||||
// XXX
|
||||
},
|
||||
|
||||
// XXX
|
||||
fitNImages: function(n){
|
||||
// XXX
|
||||
},
|
||||
|
||||
|
||||
_setup: function(viewer){
|
||||
this.viewer = $(viewer)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// Main Ribbons object...
|
||||
//
|
||||
function Ribbons(viewer){
|
||||
// in case this is called as a function (without new)...
|
||||
if(this.constructor.name != 'Ribbons'){
|
||||
return new Ribbons(viewer)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
Ribbons.__proto__ = RibbonsClassPrototype
|
||||
Ribbons.prototype = RibbonsPrototype
|
||||
Ribbons.prototype.constructor = Ribbons
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* vim:set ts=4 sw=4 : */
|
||||
Loading…
x
Reference in New Issue
Block a user