checkpoint

master
voussoir 2016-09-18 01:33:46 -07:00
commit 6c5580c1bc
30 changed files with 4958 additions and 0 deletions

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
*.key
*.csr
*.crt
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

114
etiquette/bad_search.txt Normal file
View File

@ -0,0 +1,114 @@
start_time = time.time()
# Raise for cases where the minimum > maximum
for (maxkey, maxval) in maximums.items():
if maxkey not in minimums:
continue
minval = minimums[maxkey]
if minval > maxval:
raise ValueError('Impossible min-max for %s' % maxkey)
conditions = []
minmaxers = {'<=': maximums, '>=': minimums}
# Convert the min-max parameters into query strings
print('Writing minmaxers')
for (comparator, minmaxer) in minmaxers.items():
for (field, value) in minmaxer.items():
if field not in Photo.int_properties:
raise ValueError('Unknown Photo property: %s' % field)
value = str(value)
query = min_max_query_builder(field, comparator, value)
conditions.append(query)
print(conditions)
print('Writing extension rule')
if extension is not None:
if isinstance(extension, str):
extension = [extension]
# Normalize to prevent injections
extension = [normalize_tagname(e) for e in extension]
extension = ['extension == "%s"' % e for e in extension]
extension = ' OR '.join(extension)
extension = '(%s)' % extension
conditions.append(extension)
def setify(l):
if l is None:
return set()
else:
return set(self.get_tag_by_name(t) for t in l)
tag_musts = setify(tag_musts)
tag_mays = setify(tag_mays)
tag_forbids = setify(tag_forbids)
base = '''
{negator} EXISTS(
SELECT 1 FROM photo_tag_rel
WHERE photo_tag_rel.photoid == photos.id
AND photo_tag_rel.tagid {operator} {value}
)'''
print('Writing musts')
for tag in tag_musts:
# tagid == must
query = base.format(
negator='',
operator='==',
value='"%s"' % tag.id,
)
conditions.append(query)
print('Writing mays')
if len(tag_mays) > 0:
# not any(tagid not in mays)
acceptable = tag_mays.union(tag_musts)
acceptable = ['"%s"' % t.id for t in acceptable]
acceptable = ', '.join(acceptable)
query = base.format(
negator='',
operator='IN',
value='(%s)' % acceptable,
)
conditions.append(query)
print('Writing forbids')
if len(tag_forbids) > 0:
# not any(tagid in forbids)
forbids = ['"%s"' % t.id for t in tag_forbids]
forbids = ', '.join(forbids)
query = base.format(
negator='NOT',
operator='IN',
value='(%s)' % forbids
)
conditions.append(query)
if len(conditions) == 0:
raise ValueError('No search query provided')
conditions = [query for query in conditions if query is not None]
conditions = ['(%s)' % c for c in conditions]
conditions = ' AND '.join(conditions)
conditions = 'WHERE %s' % conditions
query = 'SELECT * FROM photos %s' % conditions
query = query.replace('\n', ' ')
while ' ' in query:
query = query.replace(' ', ' ')
print(query)
temp_cur = self.sql.cursor()
temp_cur.execute(query)
for fetch in fetch_generator(temp_cur):
photo = Photo(self, fetch)
yield photo
end_time = time.time()
print(end_time - start_time)

465
etiquette/etiquette.py Normal file
View File

@ -0,0 +1,465 @@
import distutils.util
import flask
from flask import request
import json
import mimetypes
import os
import random
import re
import requests
import warnings
site = flask.Flask(__name__)
site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180,
TEMPLATES_AUTO_RELOAD=True,
)
print(os.getcwd())
import phototagger
P = phototagger.PhotoDB()
FILE_READ_CHUNK = 2 ** 20
MOTD_STRINGS = [
'Good morning, Paul. What will your first sequence of the day be?',
#'Buckle up, it\'s time to:',
]
ERROR_INVALID_ACTION = 'Invalid action'
ERROR_NO_TAG_GIVEN = 'No tag name supplied'
ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
def edit_params(original, modifications):
new_params = original.to_dict()
new_params.update(modifications)
if not new_params:
return ''
keep_params = {}
new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v]
new_params = '&'.join(new_params)
new_params = '?' + new_params
return new_params
def P_album(albumid):
try:
return P.get_album(albumid)
except phototagger.NoSuchAlbum:
flask.abort(404, 'That tag doesnt exist')
def P_photo(photoid):
try:
return P.get_photo(photoid)
except phototagger.NoSuchPhoto:
flask.abort(404, 'That photo doesnt exist')
def P_tag(tagname):
try:
return P.get_tag(tagname)
except phototagger.NoSuchTag as e:
flask.abort(404, 'That tag doesnt exist: %s' % e)
def truthystring(s):
if isinstance(s, (bool, int)) or s is None:
return s
s = s.lower()
if s in {'1', 'true', 't', 'yes', 'y', 'on'}:
return True
if s in {'null', 'none'}:
return None
return False
def read_filebytes(filepath, range_min, range_max):
range_span = range_max - range_min
#print('read span', range_min, range_max, range_span)
f = open(filepath, 'rb')
f.seek(range_min)
sent_amount = 0
with f:
while sent_amount < range_span:
chunk = f.read(FILE_READ_CHUNK)
if len(chunk) == 0:
break
yield chunk
sent_amount += len(chunk)
def send_file(filepath):
'''
Range-enabled file sending.
'''
outgoing_headers = {}
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
if 'text/' in mimetype:
mimetype += '; charset=utf-8'
outgoing_headers['Content-Type'] = mimetype
if 'range' not in request.headers:
response = flask.make_response(flask.send_file(filepath))
for (k, v) in outgoing_headers.items():
response.headers[k] = v
return response
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
desired_range = request.headers['range'].lower()
desired_range = desired_range.split('bytes=')[-1]
inthelper = lambda x: int(x) if x.isdigit() else None
if '-' in desired_range:
(desired_min, desired_max) = desired_range.split('-')
range_min = inthelper(desired_min)
range_max = inthelper(desired_max)
else:
range_min = inthelper(desired_range)
if range_min is None:
range_min = 0
if range_max is None:
range_max = file_size
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
response = flask.Response(
outgoing_data,
status=206,
headers=outgoing_headers,
)
return response
@site.route('/')
def root():
motd = random.choice(MOTD_STRINGS)
return flask.render_template('root.html', motd=motd)
@site.route('/album/<albumid>')
def get_album(albumid):
album = P_album(albumid)
response = flask.render_template(
'album.html',
album=album,
child_albums=album.children(),
photos=album.photos()
)
return response
@site.route('/file/<photoid>')
def get_file(photoid):
requested_photoid = photoid
photoid = photoid.split('.')[0]
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = truthystring(use_original_filename)
if do_download:
if use_original_filename:
download_as = photo.basename
else:
download_as = photo.id + '.' + photo.extension
# Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as
return response
else:
return send_file(photo.real_filepath)
@site.route('/albums')
def get_albums():
albums = P.get_albums()
albums = [a for a in albums if a.parent() is None]
return flask.render_template('albums.html', albums=albums)
@site.route('/photo/<photoid>', methods=['GET'])
def get_photo(photoid):
photo = P_photo(photoid)
tags = photo.tags()
tags.sort(key=lambda x: x.qualified_name())
return flask.render_template('photo.html', photo=photo, tags=tags)
@site.route('/tags')
@site.route('/tags/<specific_tag>')
def get_tags(specific_tag=None):
try:
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
except phototagger.NoSuchTag:
flask.abort(404, 'That tag doesnt exist')
tags = tags.split('\n')
tags = [t for t in tags if t != '']
tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags]
return flask.render_template('tags.html', tags=tags)
@site.route('/thumbnail/<photoid>')
def get_thumbnail(photoid):
photoid = photoid.split('.')[0]
photo = P_photo(photoid)
if photo.thumbnail:
path = photo.thumbnail
else:
flask.abort(404, 'That file doesnt have a thumbnail')
return send_file(path)
@site.route('/photo/<photoid>', methods=['POST'])
def edit_photo(photoid):
print(request.form)
response = {}
photo = P_photo(photoid)
if 'add_tag' in request.form:
action = 'add_tag'
method = photo.add_tag
elif 'remove_tag' in request.form:
action = 'remove_tag'
method = photo.remove_tag
else:
flask.abort(400, 'Invalid action')
tag = request.form[action].strip()
if tag == '':
flask.abort(400, 'No tag supplied')
try:
tag = P.get_tag(tag)
except phototagger.NoSuchTag:
return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404)
method(tag)
response['action'] = action
response['tagid'] = tag.id
response['tagname'] = tag.name
return json.dumps(response)
@site.route('/tags', methods=['POST'])
def edit_tags():
print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
method = create_tag
elif 'delete_tag_synonym' in request.form:
action = 'delete_tag_synonym'
method = delete_synonym
elif 'delete_tag' in request.form:
action = 'delete_tag'
method = delete_tag
else:
response = {'error': ERROR_INVALID_ACTION}
if status == 200:
status = 400
tag = request.form[action].strip()
if tag == '':
response = {'error': ERROR_NO_TAG_GIVEN}
try:
response = method(tag)
except phototagger.TagTooShort:
response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf:
response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e:
response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e:
response = {'error': e.args[0], 'tagname': tag}
else:
status = 200
response = json.dumps(response)
response = flask.Response(response, status=status)
return response
def create_tag(easybake_string):
notes = P.easybake(easybake_string)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
return notes
def delete_tag(tag):
tag = tag.split('.')[-1].split('+')[0]
tag = P.get_tag(tag)
tag.delete()
return {'action': 'delete_tag', 'tagname': tag.name, 'tagid': tag.id}
def delete_synonym(synonym):
synonym = synonym.split('+')[-1].split('.')[-1]
synonym = phototagger.normalize_tagname(synonym)
try:
master_tag = P.get_tag(synonym)
except phototagger.NoSuchTag:
flask.abort(404, 'That synonym doesnt exist')
if synonym not in master_tag.synonyms():
flask.abort(400, 'That name is not a synonym')
master_tag.remove_synonym(synonym)
return {'action':'delete_synonym', 'synonym': synonym}
@site.route('/search')
def search():
print(request.args)
def comma_split_helper(s):
s = s.replace(' ', ',')
s = [x.strip() for x in s.split(',')]
s = [x for x in s if x]
return s
# EXTENSION
extension_string = request.args.get('extension', '')
extension_not_string = request.args.get('extension_not', '')
mimetype_string = request.args.get('mimetype', '')
extension_list = comma_split_helper(extension_string)
extension_not_list = comma_split_helper(extension_not_string)
mimetype_list = comma_split_helper(mimetype_string)
# LIMIT
limit = request.args.get('limit', '')
if limit.isdigit():
limit = int(limit)
limit = min(100, limit)
else:
limit = 50
# OFFSET
offset = request.args.get('offset', None)
if offset:
offset = int(offset)
else:
offset = None
# MUSTS, MAYS, FORBIDS
qualname_map = P.export_tags(exporter=phototagger.tag_export_qualname_map)
tag_musts = request.args.get('tag_musts', '').split(',')
tag_mays = request.args.get('tag_mays', '').split(',')
tag_forbids = request.args.get('tag_forbids', '').split(',')
tag_expression = request.args.get('tag_expression', None)
tag_musts = [qualname_map.get(tag, tag) for tag in tag_musts if tag != '']
tag_mays = [qualname_map.get(tag, tag) for tag in tag_mays if tag != '']
tag_forbids = [qualname_map.get(tag, tag) for tag in tag_forbids if tag != '']
# ORDERBY
orderby = request.args.get('orderby', None)
if orderby:
orderby = orderby.replace('-', ' ')
orderby = orderby.replace('_', ' ')
orderby = orderby.split(',')
else:
orderby = None
# HAS_TAGS
has_tags = request.args.get('has_tags', '')
if has_tags == '':
has_tags = None
else:
has_tags = truthystring(has_tags)
# MINMAXERS
area = request.args.get('area', None)
width = request.args.get('width', None)
height = request.args.get('height', None)
ratio = request.args.get('ratio', None)
bytes = request.args.get('bytes', None)
length = request.args.get('length', None)
created = request.args.get('created', None)
# These are in a dictionary so I can pass them to the page template.
search_kwargs = {
'area': area,
'width': width,
'height': height,
'ratio': ratio,
'bytes': bytes,
'length': length,
'created': created,
'extension': extension_list,
'extension_not': extension_not_list,
'has_tags': has_tags,
'mimetype': mimetype_list,
'tag_musts': tag_musts,
'tag_mays': tag_mays,
'tag_forbids': tag_forbids,
'tag_expression': tag_expression,
'limit': limit,
'offset': offset,
'orderby': orderby,
'warn_bad_tags': True,
}
print(search_kwargs)
with warnings.catch_warnings(record=True) as catcher:
photos = list(P.search(**search_kwargs))
warns = [str(warning.message) for warning in catcher]
print(warns)
total_tags = set()
for photo in photos:
total_tags.update(photo.tags())
for tag in total_tags:
tag._cached_qualname = qualname_map[tag.name]
total_tags = sorted(total_tags, key=lambda x: x._cached_qualname)
# PREV-NEXT PAGE URLS
offset = offset or 0
if len(photos) == limit:
next_params = edit_params(request.args, {'offset': offset + limit})
next_page_url = '/search' + next_params
else:
next_page_url = None
if offset > 0:
prev_params = edit_params(request.args, {'offset': max(0, offset - limit)})
prev_page_url = '/search' + prev_params
else:
prev_page_url = None
search_kwargs['extension'] = extension_string
search_kwargs['extension_not'] = extension_not_string
search_kwargs['mimetype'] = mimetype_string
response = flask.render_template(
'search.html',
photos=photos,
search_kwargs=search_kwargs,
total_tags=total_tags,
prev_page_url=prev_page_url,
next_page_url=next_page_url,
qualname_map=json.dumps(qualname_map),
warns=warns,
)
return response
@site.route('/static/<filename>')
def get_resource(filename):
print(filename)
return flask.send_file('.\\static\\%s' % filename)
if __name__ == '__main__':
site.run(threaded=True)

View File

@ -0,0 +1,25 @@
# Use with
# py -i etiquette_easy.py
import phototagger
P = phototagger.PhotoDB()
import traceback
def easytagger():
while True:
i = input('> ')
if i.startswith('?'):
i = i.split('?')[1] or None
try:
P.export_tags(specific_tag=i)
except:
traceback.print_exc()
continue
P.easybake(i)
def photag(photoid):
photo = P.get_photo_by_id(photoid)
print(photo.tags())
while True:
photo.add_tag(input('> '))
get=P.get_tag

View File

@ -0,0 +1,25 @@
from gevent import monkey
monkey.patch_all()
import etiquette
import gevent.pywsgi
import gevent.wsgi
import sys
if len(sys.argv) == 2:
port = int(sys.argv[1])
else:
port = 5000
if port == 443:
http = gevent.pywsgi.WSGIServer(
('', port),
etiquette.site,
keyfile='etiquette.key',
certfile='etiquette.crt',
)
else:
http = gevent.wsgi.WSGIServer(('', port), etiquette.site)
print('Starting server')
http.serve_forever()

1955
etiquette/phototagger.py Normal file

File diff suppressed because it is too large Load Diff

16
etiquette/reider.py Normal file
View File

@ -0,0 +1,16 @@
for p in P.get_photos():
g=P.cur.execute('UPDATE photos SET id==? WHERE id==?', [p.id[-12:], p.id])
g=P.cur.execute('UPDATE photo_tag_rel SET photoid==? WHERE photoid==?', [p.id[-12:], p.id])
g=P.cur.execute('UPDATE album_photo_rel SET photoid==? WHERE photoid==?', [p.id[-12:], p.id])
for t in P.get_tags():
g=P.cur.execute('UPDATE tags SET id==? WHERE id==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE photo_tag_rel SET tagid==? WHERE tagid==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE tag_group_rel SET parentid==? WHERE parentid==?', [t.id[-12:], t.id])
g=P.cur.execute('UPDATE tag_group_rel SET memberid==? WHERE memberid==?', [t.id[-12:], t.id])
for a in P.get_albums():
g=P.cur.execute('UPDATE albums SET id==? WHERE id==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE tag_group_rel SET parentid==? WHERE parentid==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE tag_group_rel SET memberid==? WHERE memberid==?', [a.id[-12:], a.id])
g=P.cur.execute('UPDATE album_photo_rel SET albumid==? WHERE albumid==?', [a.id[-12:], a.id])

BIN
etiquette/samples/bolts.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

BIN
etiquette/samples/train.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="audio.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\audio.png"
inkscape:export-xdpi="144"
inkscape:export-ydpi="144">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4723">
<stop
style="stop-color:#157a05;stop-opacity:1"
offset="0"
id="stop4725" />
<stop
style="stop-color:#8cec27;stop-opacity:1"
offset="1"
id="stop4727" />
</linearGradient>
<clipPath
id="clipPath5600"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path5602"
d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4723"
id="linearGradient4739"
x1="196.13852"
y1="903.96155"
x2="177.04013"
y2="792.60211"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.67124999"
inkscape:cx="199.31367"
inkscape:cy="266.33033"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px"
showguides="false" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
id="path4146"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d=""
inkscape:connector-curvature="0" />
<path
cx="356.42856"
cy="515.93359"
r="190.61275"
id="path4148"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d=""
inkscape:connector-curvature="0" />
<g
id="g4759"
transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)">
<path
id="path4176"
d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z"
style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccc" />
<path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879"
id="path4179"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
<ellipse
transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)"
ry="71.356659"
rx="39.683559"
cy="753.57629"
cx="-400.81314"
id="path4189"
style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:connector-curvature="0"
id="circle5376"
d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z"
style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="other.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\other.png"
inkscape:export-xdpi="185.87027"
inkscape:export-ydpi="185.87027">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4361">
<stop
style="stop-color:#4d4d4d;stop-opacity:1"
offset="0"
id="stop4363" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4365" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4361"
id="linearGradient4367"
x1="315.47162"
y1="1037.2178"
x2="204.01379"
y2="699.85175"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.94929084"
inkscape:cx="51.384844"
inkscape:cy="169.83481"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<rect
style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4359"
width="251.81471"
height="251.8147"
x="74.092659"
y="726.45483" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="400"
viewBox="0 0 400.00002 400"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="video.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\video.png"
inkscape:export-xdpi="143.99998"
inkscape:export-ydpi="143.99998">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4160">
<stop
style="stop-color:#414141;stop-opacity:1"
offset="0"
id="stop4162" />
<stop
id="stop4172"
offset="0.44465271"
style="stop-color:#a5a5a5;stop-opacity:1" />
<stop
id="stop4168"
offset="0.51481694"
style="stop-color:#cccccc;stop-opacity:1" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4164" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4160"
id="linearGradient4166"
x1="218.28458"
y1="950.68616"
x2="213.38019"
y2="742.12012"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.335625"
inkscape:cx="833.60008"
inkscape:cy="34.45227"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36221)">
<path
style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:transform-center-x="-38.11878"
inkscape:transform-center-y="-2.5060727e-005"
d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z"
id="path4157"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cczcc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

171
etiquette/static/common.css Normal file
View File

@ -0,0 +1,171 @@
body
{
display: flex;
flex-direction: column;
background-color:#00d8f4;
margin: 8px;
}
li
{
position:relative;
}
#header
{
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
margin-bottom: 4px;
}
.header_element
{
display: flex;
justify-content: center;
flex: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.header_element:hover
{
background-color: #ffffd4;
}
#content_body
{
flex: 0 0 auto;
display: flex;
flex-direction: row;
}
.add_tag_button, #search_go_button
{
border-top: 2px solid #c2ffc3;
border-left: 2px solid #c2ffc3;
border-right: 2px solid #259427;
border-bottom: 2px solid #259427;
background-color: #6df16f;
}
.add_tag_button:active, #search_go_button:active
{
border-top: 2px solid #259427;
border-left: 2px solid #259427;
border-right: 2px solid #c2ffc3;
border-bottom: 2px solid #c2ffc3;
}
.remove_tag_button,
.remove_tag_button_perm
{
position: absolute;
top: 3px;
width: 18px;
height: 14px;
padding: 0;
vertical-align: middle;
background-color: #ff4949;
border-top: 2px solid #ffacac;
border-left: 2px solid #ffacac;
border-right: 2px solid #bd1b1b;
border-bottom: 2px solid #bd1b1b;
}
.remove_tag_button
{
display: none;
}
.tag_object:hover + .remove_tag_button,
.remove_tag_button:hover,
.remove_tag_button_perm:hover
{
display:inline;
}
.remove_tag_button:active,
.remove_tag_button_perm:active
{
border-top: 2px solid #bd1b1b;
border-left: 2px solid #bd1b1b;
border-right: 2px solid #ffacac;
border-bottom: 2px solid #ffacac;
}
.photo_galleryview
{
vertical-align: middle;
position: relative;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
display: inline-block;
min-width: 150px;
max-width: 300px;
height: 200px;
background-color: #ffffd4;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
.photo_galleryview_thumb
{
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.photo_galleryview_thumb a
{
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.photo_galleryview_thumb img
{
max-width: 100%;
max-height: 100%;
height: auto;
}
.photo_galleryview_info
{
position: absolute;
top: 160px;
bottom: 0px;
left: 8px;
right: 8px;
font-size: 0.8em;
}
.photo_galleryview_info a
{
position: absolute;
max-height: 30px;
overflow: hidden;
}
.photo_galleryview_info a:hover
{
max-height: 100%;
}
.photo_galleryview_info span
{
position: absolute;
bottom: 0;
right: 0;
}
.tag_object
{
font-size: 0.9em;
background-color: #fff;
border-radius: 2px;
text-decoration: none;
font-family: monospace;
line-height: 1.3;
}
.callback_message_positive, .callback_message_negative
{
width: 80%;
margin: 4px;
}
.callback_message_positive
{
background-color: #afa;
}
.callback_message_negative
{
background-color: #faa;
}

View File

@ -0,0 +1,84 @@
function create_message_bubble(message_positivity, message_text, lifespan)
{
if (lifespan === undefined)
{
lifespan = 8000;
}
var message = document.createElement("div");
message.className = message_positivity;
var span = document.createElement("span");
span.innerHTML = message_text;
message.appendChild(span);
message_area.appendChild(message);
setTimeout(function(){message_area.removeChild(message);}, lifespan);
}
function add_photo_tag(photoid, tagname, callback)
{
if (tagname === ""){return}
var url = "/photo/" + photoid;
data = new FormData();
data.append("add_tag", tagname);
return post(url, data, callback);
}
function remove_photo_tag(photoid, tagname, callback)
{
if (tagname === ""){return}
var url = "/photo/" + photoid;
data = new FormData();
data.append("remove_tag", tagname);
return post(url, data, callback);
}
function edit_tags(action, name, callback)
{
if (name === ""){return}
var url = "/tags";
data = new FormData();
data.append(action, name);
return post(url, data, callback);
}
function delete_tag_synonym(name, callback)
{
return edit_tags("delete_tag_synonym", name, callback);
}
function delete_tag(name, callback)
{
return edit_tags("delete_tag", name, callback);
}
function create_tag(name, callback)
{
return edit_tags("create_tag", name, callback);
}
function post(url, data, callback)
{
var request = new XMLHttpRequest();
request.answer = null;
request.onreadystatechange = function()
{
if (request.readyState == 4)
{
var text = request.responseText;
if (callback != null)
{
console.log(text);
callback(JSON.parse(text));
}
}
};
var asynchronous = true;
request.open("POST", url, asynchronous);
request.send(data);
}
function bind_box_to_button(box, button)
{
box.onkeydown=function()
{
if (event.keyCode == 13)
{
button.click();
}
};
}

View File

@ -0,0 +1,403 @@
/**
* complete.ly 1.0.0
* MIT Licensing
* Copyright (c) 2013 Lorenzo Puccetti
*
* This Software shall be used for doing good things, not bad things.
*
**/
function completely(container, config) {
config = config || {};
config.fontSize = config.fontSize || '16px';
config.fontFamily = config.fontFamily || 'sans-serif';
config.promptInnerHTML = config.promptInnerHTML || '';
config.color = config.color || '#333';
config.hintColor = config.hintColor || '#aaa';
config.backgroundColor = config.backgroundColor || '#fff';
config.dropDownBorderColor = config.dropDownBorderColor || '#aaa';
config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody
config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd';
var txtInput = document.createElement('input');
txtInput.type ='text';
txtInput.spellcheck = false;
txtInput.style.fontSize = config.fontSize;
txtInput.style.fontFamily = config.fontFamily;
txtInput.style.color = config.color;
txtInput.style.backgroundColor = config.backgroundColor;
txtInput.style.width = '100%';
txtInput.style.outline = '0';
txtInput.style.border = '0';
txtInput.style.margin = '0';
txtInput.style.padding = '0';
var txtHint = txtInput.cloneNode();
txtHint.disabled='';
txtHint.style.position = 'absolute';
txtHint.style.top = '0';
txtHint.style.left = '0';
txtHint.style.borderColor = 'transparent';
txtHint.style.boxShadow = 'none';
txtHint.style.color = config.hintColor;
txtInput.style.backgroundColor ='transparent';
txtInput.style.verticalAlign = 'top';
txtInput.style.position = 'relative';
var wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.outline = '0';
wrapper.style.border = '0';
wrapper.style.margin = '0';
wrapper.style.padding = '0';
var prompt = document.createElement('div');
prompt.style.position = 'absolute';
prompt.style.outline = '0';
prompt.style.margin = '0';
prompt.style.padding = '0';
prompt.style.border = '0';
prompt.style.fontSize = config.fontSize;
prompt.style.fontFamily = config.fontFamily;
prompt.style.color = config.color;
prompt.style.backgroundColor = config.backgroundColor;
prompt.style.top = '0';
prompt.style.left = '0';
prompt.style.overflow = 'hidden';
prompt.innerHTML = config.promptInnerHTML;
prompt.style.background = 'transparent';
if (document.body === undefined) {
throw 'document.body is undefined. The library was wired up incorrectly.';
}
document.body.appendChild(prompt);
var w = prompt.getBoundingClientRect().right; // works out the width of the prompt.
wrapper.appendChild(prompt);
prompt.style.visibility = 'visible';
prompt.style.left = '-'+w+'px';
wrapper.style.marginLeft= w+'px';
wrapper.appendChild(txtHint);
wrapper.appendChild(txtInput);
var dropDown = document.createElement('div');
dropDown.style.position = 'absolute';
dropDown.style.visibility = 'hidden';
dropDown.style.outline = '0';
dropDown.style.margin = '0';
dropDown.style.padding = '0';
dropDown.style.textAlign = 'left';
dropDown.style.fontSize = config.fontSize;
dropDown.style.fontFamily = config.fontFamily;
dropDown.style.backgroundColor = config.backgroundColor;
dropDown.style.zIndex = config.dropDownZIndex;
dropDown.style.cursor = 'default';
dropDown.style.borderStyle = 'solid';
dropDown.style.borderWidth = '1px';
dropDown.style.borderColor = config.dropDownBorderColor;
dropDown.style.overflowX= 'hidden';
dropDown.style.whiteSpace = 'pre';
dropDown.style.overflowY = 'scroll'; // note: this might be ugly when the scrollbar is not required. however in this way the width of the dropDown takes into account
var createDropDownController = function(elem) {
var rows = [];
var ix = 0;
var oldIndex = -1;
var onMouseOver = function() { this.style.outline = '1px solid #ddd'; }
var onMouseOut = function() { this.style.outline = '0'; }
var onMouseDown = function() { p.hide(); p.onmouseselection(this.__hint); }
var p = {
hide : function() { elem.style.visibility = 'hidden'; },
refresh : function(token, array) {
elem.style.visibility = 'hidden';
ix = 0;
elem.innerHTML ='';
var vph = (window.innerHeight || document.documentElement.clientHeight);
var rect = elem.parentNode.getBoundingClientRect();
var distanceToTop = rect.top - 6; // heuristic give 6px
var distanceToBottom = vph - rect.bottom -6; // distance from the browser border.
rows = [];
for (var i=0;i<array.length;i++) {
if (array[i].indexOf(token)!==0) { continue; }
var divRow =document.createElement('div');
divRow.style.color = config.color;
divRow.onmouseover = onMouseOver;
divRow.onmouseout = onMouseOut;
divRow.onmousedown = onMouseDown;
divRow.__hint = array[i];
divRow.innerHTML = token+'<b>'+array[i].substring(token.length)+'</b>';
rows.push(divRow);
elem.appendChild(divRow);
}
if (rows.length===0) {
return; // nothing to show.
}
if (rows.length===1 && token === rows[0].__hint) {
return; // do not show the dropDown if it has only one element which matches what we have just displayed.
}
if (rows.length<2) return;
p.highlight(0);
if (distanceToTop > distanceToBottom*3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom
elem.style.maxHeight = distanceToTop+'px'; // we display the dropDown on the top of the input text
elem.style.top ='';
elem.style.bottom ='100%';
} else {
elem.style.top = '100%';
elem.style.bottom = '';
elem.style.maxHeight = distanceToBottom+'px';
}
elem.style.visibility = 'visible';
},
highlight : function(index) {
if (oldIndex !=-1 && rows[oldIndex]) {
rows[oldIndex].style.backgroundColor = config.backgroundColor;
}
rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config
oldIndex = index;
},
move : function(step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1.
if (elem.style.visibility === 'hidden') return ''; // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up)
if (ix+step === -1 || ix+step === rows.length) return rows[ix].__hint; // NO CIRCULAR SCROLLING.
ix+=step;
p.highlight(ix);
return rows[ix].__hint;//txtShadow.value = uRows[uIndex].__hint ;
},
onmouseselection : function() {} // it will be overwritten.
};
return p;
}
var dropDownController = createDropDownController(dropDown);