added gen4 ui prototype...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2014-07-20 02:25:36 +04:00
parent ece95ff30f
commit a458b592e7
24 changed files with 17924 additions and 0 deletions

249
ui (gen4)/Makefile Executable file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

770
ui (gen4)/ext-lib/jstorage.js Executable file
View 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

File diff suppressed because one or more lines are too long

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
View 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
View 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
View 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
View 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
View 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
View 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">&times;</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

File diff suppressed because it is too large Load Diff

795
ui (gen4)/lib/keyboard.js Executable file
View 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
View 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() == '' ? '&nbsp;' : 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('&times;'))
: $('<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() == '' ? '&nbsp;' : 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('&times;'))
: $('<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
View 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
View File

@ -0,0 +1,16 @@
/**********************************************************************
*
*
*
**********************************************************************/
//var DEBUG = DEBUG != null ? DEBUG : true
/*********************************************************************/
/**********************************************************************
* vim:set ts=4 sw=4 : */

16
ui (gen4)/package.json Executable file
View 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
View 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 : */