edit submissions (with tags)
This commit is contained in:
parent
b7fc462c14
commit
d52b149d04
@ -3,28 +3,76 @@
|
|||||||
module Handler.EditSubmission where
|
module Handler.EditSubmission where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
import Handler.Common (checkIfCanEdit)
|
||||||
|
import Handler.SubmissionView
|
||||||
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, bfs)
|
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, bfs)
|
||||||
|
|
||||||
|
import Data.Text as T
|
||||||
|
|
||||||
getEditSubmissionR :: SubmissionId -> Handler Html
|
getEditSubmissionR :: SubmissionId -> Handler Html
|
||||||
getEditSubmissionR submissionId = do
|
getEditSubmissionR submissionId = do
|
||||||
(formWidget, formEnctype) <- generateFormPost editSubmissionForm
|
submission <- runDB $ get404 submissionId
|
||||||
|
tags <- runDB $ getTags submissionId
|
||||||
|
let mTagsAsText = case tags of
|
||||||
|
[] -> Nothing
|
||||||
|
_ -> Just $ T.intercalate ", " $ Import.map tagName $ Import.catMaybes tags
|
||||||
|
(formWidget, formEnctype) <- generateFormPost $ editSubmissionForm (submissionDescription submission) mTagsAsText
|
||||||
doEditSubmission formWidget formEnctype submissionId
|
doEditSubmission formWidget formEnctype submissionId
|
||||||
|
|
||||||
postEditSubmissionR :: SubmissionId -> Handler Html
|
postEditSubmissionR :: SubmissionId -> Handler Html
|
||||||
postEditSubmissionR submissionId = do
|
postEditSubmissionR submissionId = do
|
||||||
((result, formWidget), formEnctype) <- runFormPost editSubmissionForm
|
submission <- runDB $ get404 submissionId
|
||||||
doEditSubmission formWidget formEnctype submissionId
|
((result, _), _) <- runFormPost $ editSubmissionForm (submissionDescription submission) Nothing
|
||||||
|
let FormSuccess (description, tags) = result
|
||||||
|
isEditable <- checkIfCanEdit submissionId
|
||||||
|
if isEditable
|
||||||
|
then
|
||||||
|
runDB $ do
|
||||||
|
update submissionId [SubmissionDescription =. description]
|
||||||
|
|
||||||
|
sts <- selectList [SubmissionTagSubmission ==. submissionId] []
|
||||||
|
let currentTagIds = Import.map (submissionTagTag . entityVal) sts
|
||||||
|
|
||||||
|
addTags submissionId tags currentTagIds
|
||||||
|
|
||||||
|
return ()
|
||||||
|
else
|
||||||
|
do
|
||||||
|
setMessage $ toHtml ("Only owner can edit a submission!!!" :: Text)
|
||||||
|
return ()
|
||||||
|
getEditSubmissionR submissionId
|
||||||
|
|
||||||
|
|
||||||
|
addTags submissionId tagsAsText existingOnes = do
|
||||||
|
let newTags = case tagsAsText of
|
||||||
|
Just tags' -> Import.map T.strip $ T.split (== ',') tags'
|
||||||
|
Nothing -> []
|
||||||
|
mTs <- mapM (\t -> getBy $ UniqueTagName t) newTags
|
||||||
|
let tids = Import.map entityKey $ Import.catMaybes mTs
|
||||||
|
|
||||||
|
deleteWhere [SubmissionTagSubmission ==. submissionId, SubmissionTagTag /<-. tids]
|
||||||
|
|
||||||
|
_ <- mapM (\tid -> insert $ SubmissionTag submissionId tid Nothing) (Import.filter (not . (`elem` existingOnes)) tids)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
doEditSubmission formWidget formEnctype submissionId = do
|
doEditSubmission formWidget formEnctype submissionId = do
|
||||||
submission <- runDB $ get404 submissionId
|
submission <- runDB $ get404 submissionId
|
||||||
|
submissionFull <- getFullInfo (Entity submissionId submission)
|
||||||
|
let view = queryResult submissionFull
|
||||||
|
|
||||||
|
tagsAvailable <- runDB $ selectList [] [Asc TagName]
|
||||||
|
let tagsAvailableAsJSON = toJSON $ Import.map (tagName . entityVal) tagsAvailable
|
||||||
|
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitle "Edit a submission"
|
setTitle "Edit a submission"
|
||||||
$(widgetFile "edit-submission")
|
$(widgetFile "edit-submission")
|
||||||
|
|
||||||
editSubmissionForm :: Form (Text, Maybe Text)
|
editSubmissionForm :: Text -> Maybe Text -> Form (Text, Maybe Text)
|
||||||
editSubmissionForm = renderBootstrap3 BootstrapBasicForm $ (,)
|
editSubmissionForm description mTags = renderBootstrap3 BootstrapBasicForm $ (,)
|
||||||
<$> areq textField (bfs MsgSubmissionDescription) Nothing
|
<$> areq textField (bfs MsgSubmissionDescription) (Just description)
|
||||||
<*> aopt textField (tagsfs MsgSubmissionTags) Nothing
|
<*> aopt textField (tagsfs MsgSubmissionTags) (Just mTags)
|
||||||
|
|
||||||
tagsfs :: RenderMessage site msg => msg -> FieldSettings site
|
tagsfs :: RenderMessage site msg => msg -> FieldSettings site
|
||||||
tagsfs msg = attrs { fsAttrs = ("data-role"::Text,"tagsinput"::Text):(fsAttrs attrs)}
|
tagsfs msg = attrs { fsAttrs = ("data-role"::Text,"tagsinput"::Text):(fsAttrs attrs)}
|
||||||
|
@ -3,6 +3,7 @@ module Handler.Query where
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Handler.Shared
|
import Handler.Shared
|
||||||
|
import Handler.SubmissionView
|
||||||
import PersistSHA1
|
import PersistSHA1
|
||||||
|
|
||||||
import Database.Persist.Sql
|
import Database.Persist.Sql
|
||||||
@ -11,24 +12,6 @@ import Data.Text as T(pack)
|
|||||||
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3,
|
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3,
|
||||||
withSmallInput)
|
withSmallInput)
|
||||||
|
|
||||||
data FullSubmissionInfo = FullSubmissionInfo {
|
|
||||||
fsiSubmissionId :: SubmissionId,
|
|
||||||
fsiSubmission :: Submission,
|
|
||||||
fsiUser :: User,
|
|
||||||
fsiRepo :: Repo,
|
|
||||||
fsiChallenge :: Challenge }
|
|
||||||
|
|
||||||
getFullInfo :: Entity Submission -> Handler FullSubmissionInfo
|
|
||||||
getFullInfo (Entity submissionId submission) = do
|
|
||||||
repo <- runDB $ get404 $ submissionRepo submission
|
|
||||||
user <- runDB $ get404 $ submissionSubmitter submission
|
|
||||||
challenge <- runDB $ get404 $ submissionChallenge submission
|
|
||||||
return $ FullSubmissionInfo {
|
|
||||||
fsiSubmissionId = submissionId,
|
|
||||||
fsiSubmission = submission,
|
|
||||||
fsiUser = user,
|
|
||||||
fsiRepo = repo,
|
|
||||||
fsiChallenge = challenge }
|
|
||||||
|
|
||||||
findSubmissions :: Text -> Handler [FullSubmissionInfo]
|
findSubmissions :: Text -> Handler [FullSubmissionInfo]
|
||||||
findSubmissions sha1Prefix = do
|
findSubmissions sha1Prefix = do
|
||||||
@ -69,14 +52,5 @@ processQuery query = do
|
|||||||
setTitle "query results"
|
setTitle "query results"
|
||||||
$(widgetFile "query-results")
|
$(widgetFile "query-results")
|
||||||
|
|
||||||
queryResult submission = do
|
|
||||||
$(widgetFile "query-result")
|
|
||||||
where commitSha1AsText = fromSHA1ToText $ submissionCommit $ fsiSubmission submission
|
|
||||||
submitter = formatSubmitter $ fsiUser submission
|
|
||||||
publicSubmissionBranch = getPublicSubmissionBranch $ fsiSubmissionId submission
|
|
||||||
publicSubmissionRepo = getReadOnlySubmissionUrl $ challengeName $ fsiChallenge submission
|
|
||||||
browsableUrl = browsableGitRepoBranch (challengeName $ fsiChallenge submission) publicSubmissionBranch
|
|
||||||
stamp = T.pack $ show $ submissionStamp $ fsiSubmission submission
|
|
||||||
|
|
||||||
queryForm :: Form Text
|
queryForm :: Form Text
|
||||||
queryForm = renderBootstrap3 BootstrapBasicForm $ areq textField (fieldSettingsLabel MsgGitCommitSha1) Nothing
|
queryForm = renderBootstrap3 BootstrapBasicForm $ areq textField (fieldSettingsLabel MsgGitCommitSha1) Nothing
|
||||||
|
40
Handler/SubmissionView.hs
Normal file
40
Handler/SubmissionView.hs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
module Handler.SubmissionView where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Shared
|
||||||
|
import PersistSHA1
|
||||||
|
|
||||||
|
import Data.Text as T(pack)
|
||||||
|
|
||||||
|
data FullSubmissionInfo = FullSubmissionInfo {
|
||||||
|
fsiSubmissionId :: SubmissionId,
|
||||||
|
fsiSubmission :: Submission,
|
||||||
|
fsiUser :: User,
|
||||||
|
fsiRepo :: Repo,
|
||||||
|
fsiChallenge :: Challenge }
|
||||||
|
|
||||||
|
getFullInfo :: Entity Submission -> Handler FullSubmissionInfo
|
||||||
|
getFullInfo (Entity submissionId submission) = do
|
||||||
|
repo <- runDB $ get404 $ submissionRepo submission
|
||||||
|
user <- runDB $ get404 $ submissionSubmitter submission
|
||||||
|
challenge <- runDB $ get404 $ submissionChallenge submission
|
||||||
|
return $ FullSubmissionInfo {
|
||||||
|
fsiSubmissionId = submissionId,
|
||||||
|
fsiSubmission = submission,
|
||||||
|
fsiUser = user,
|
||||||
|
fsiRepo = repo,
|
||||||
|
fsiChallenge = challenge }
|
||||||
|
|
||||||
|
queryResult submission = do
|
||||||
|
$(widgetFile "query-result")
|
||||||
|
where commitSha1AsText = fromSHA1ToText $ submissionCommit $ fsiSubmission submission
|
||||||
|
submitter = formatSubmitter $ fsiUser submission
|
||||||
|
publicSubmissionBranch = getPublicSubmissionBranch $ fsiSubmissionId submission
|
||||||
|
publicSubmissionRepo = getReadOnlySubmissionUrl $ challengeName $ fsiChallenge submission
|
||||||
|
browsableUrl = browsableGitRepoBranch (challengeName $ fsiChallenge submission) publicSubmissionBranch
|
||||||
|
stamp = T.pack $ show $ submissionStamp $ fsiSubmission submission
|
||||||
|
|
||||||
|
getTags submissionId = do
|
||||||
|
sts <- selectList [SubmissionTagSubmission ==. submissionId] []
|
||||||
|
tags <- mapM get $ Import.map (submissionTagTag . entityVal) sts
|
||||||
|
return tags
|
@ -78,4 +78,9 @@ Tag
|
|||||||
name Text
|
name Text
|
||||||
description Text Maybe
|
description Text Maybe
|
||||||
UniqueTagName name
|
UniqueTagName name
|
||||||
|
SubmissionTag
|
||||||
|
submission SubmissionId
|
||||||
|
tag TagId
|
||||||
|
accepted Bool Maybe
|
||||||
|
UniqueSubmissionTag submission tag
|
||||||
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
-- By default this file is used in Model.hs (which is imported by Foundation.hs)
|
||||||
|
@ -47,6 +47,7 @@ library
|
|||||||
Handler.Presentation
|
Handler.Presentation
|
||||||
Handler.Tags
|
Handler.Tags
|
||||||
Handler.EditSubmission
|
Handler.EditSubmission
|
||||||
|
Handler.SubmissionView
|
||||||
|
|
||||||
if flag(dev) || flag(library-only)
|
if flag(dev) || flag(library-only)
|
||||||
cpp-options: -DDEVELOPMENT
|
cpp-options: -DDEVELOPMENT
|
||||||
|
212
static/css/tagify.css
Normal file
212
static/css/tagify.css
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
tags {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
padding-right: 0.3em 0.5em;
|
||||||
|
cursor: text;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags:hover {
|
||||||
|
border-color: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px 0 5px 5px;
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
-webkit-transition: .13s ease-out;
|
||||||
|
transition: .13s ease-out;
|
||||||
|
-webkit-animation: .3s tags--bump 1 ease-out;
|
||||||
|
animation: .3s tags--bump 1 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag > div {
|
||||||
|
vertical-align: top;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
color: black;
|
||||||
|
-webkit-transition: .13s ease-out;
|
||||||
|
transition: .13s ease-out;
|
||||||
|
padding-right: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag > div > span {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-transition: .1s;
|
||||||
|
transition: .1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag > div::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #E5E5E5;
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-transition: 80ms ease;
|
||||||
|
transition: 80ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag:hover div::before {
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
background: #D3E2E2;
|
||||||
|
box-shadow: 0 0 0 0 #D39494 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag.tagify--noAnim {
|
||||||
|
-webkit-animation: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag.tagify--hide {
|
||||||
|
width: 0 !important;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-transition: .3s;
|
||||||
|
transition: .3s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag.tagify--mark div::before {
|
||||||
|
-webkit-animation: .3s tagify--pulse 2 ease-out;
|
||||||
|
animation: .3s tagify--pulse 2 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag.tagify--notAllowed div > span {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag.tagify--notAllowed div::before {
|
||||||
|
background: rgba(211, 148, 148, 0.44);
|
||||||
|
-webkit-transition: .2s;
|
||||||
|
transition: .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag x {
|
||||||
|
font: 14px/14px Serif;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
right: -webkit-calc(0.5em - 2px);
|
||||||
|
right: calc(0.5em - 2px);
|
||||||
|
top: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-transform: translateY(-50%);
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
-webkit-transition: .2s ease-out;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag x::after {
|
||||||
|
content: "\00D7";
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag x:hover {
|
||||||
|
color: white;
|
||||||
|
background: #c77777;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag x:hover + div > span {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags tag x:hover + div::before {
|
||||||
|
background: rgba(211, 148, 148, 0.44);
|
||||||
|
-webkit-transition: .2s;
|
||||||
|
transition: .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags input,tags textarea {
|
||||||
|
border: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags > div {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 10px;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags > div > input {
|
||||||
|
display: block;
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags > div > input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags > div > input.placeholder ~ span {
|
||||||
|
opacity: .5;
|
||||||
|
-webkit-transform: none;
|
||||||
|
-ms-transform: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags > div > span {
|
||||||
|
opacity: 0;
|
||||||
|
line-height: 1.8;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-transform: translatex(6px);
|
||||||
|
-ms-transform: translatex(6px);
|
||||||
|
transform: translatex(6px);
|
||||||
|
-webkit-transition: .15s ease-out;
|
||||||
|
transition: .15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes tags--bump {
|
||||||
|
30% {
|
||||||
|
box-shadow: 0 0 0 4px #E5E5E5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tags--bump {
|
||||||
|
30% {
|
||||||
|
box-shadow: 0 0 0 4px #E5E5E5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes tagify--pulse {
|
||||||
|
25% {
|
||||||
|
background: rgba(211, 148, 148, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tagify--pulse {
|
||||||
|
25% {
|
||||||
|
background: rgba(211, 148, 148, 0.6);
|
||||||
|
}
|
||||||
|
}
|
238
static/js/tagify.js
Normal file
238
static/js/tagify.js
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Tagify - jQuery tags input plugin
|
||||||
|
* By Yair Even-Or (2016)
|
||||||
|
* Don't sell this code. (c)
|
||||||
|
* https://github.com/yairEO/tagify
|
||||||
|
*/
|
||||||
|
function Tagify( input, settings ){
|
||||||
|
// protection
|
||||||
|
if( !input ){
|
||||||
|
console.warn('Tagify: ', 'invalid input element ', input)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = typeof settings == 'object' ? settings : {}; // make sure settings is an 'object'
|
||||||
|
|
||||||
|
this.settings = {
|
||||||
|
duplicates : settings.duplicates || false, // flag - allow tuplicate tags
|
||||||
|
enforeWhitelist : settings.enforeWhitelist || false, // flag - should ONLY use tags allowed in whitelist
|
||||||
|
autocomplete : settings.autocomplete || true, // flag - show native suggeestions list as you type
|
||||||
|
whitelist : settings.whitelist || [], // is this list has any items, then only allow tags from this list
|
||||||
|
blacklist : settings.blacklist || [] // a list of non-allowed tags
|
||||||
|
};
|
||||||
|
|
||||||
|
this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it)
|
||||||
|
this.value = []; // An array holding all the (currently used) tags
|
||||||
|
this.DOM = {}; // Store all relevant DOM elements in an Object
|
||||||
|
this.build(input);
|
||||||
|
this.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
Tagify.prototype = {
|
||||||
|
build : function( input ){
|
||||||
|
var that = this,
|
||||||
|
value = input.value;
|
||||||
|
|
||||||
|
this.DOM.originalInput = input;
|
||||||
|
this.DOM.scope = document.createElement('tags');
|
||||||
|
this.DOM.scope.innerHTML = '<div><input list="tagsSuggestions'+ this.id +'" class="placeholder"/><span>'+ input.placeholder +'</span></div>';
|
||||||
|
|
||||||
|
this.DOM.input = this.DOM.scope.querySelector('input');
|
||||||
|
input.parentNode.insertBefore(this.DOM.scope, input);
|
||||||
|
this.DOM.scope.appendChild(input);
|
||||||
|
|
||||||
|
// if "autocomplete" flag on toggeled & "whitelist" has items, build suggestions list
|
||||||
|
if( this.settings.autocomplete && this.settings.whitelist.length )
|
||||||
|
this.buildDataList();
|
||||||
|
|
||||||
|
// if the original input already had any value (tags)
|
||||||
|
if( value )
|
||||||
|
this.addTag(value).forEach(function(tag){
|
||||||
|
tag && tag.classList.add('tagify--noAnim');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM events binding
|
||||||
|
*/
|
||||||
|
events : function(){
|
||||||
|
var events = {
|
||||||
|
// event name / event callback / element to be listening to
|
||||||
|
focus : ['onFocusBlur' , 'input'],
|
||||||
|
blur : ['onFocusBlur' , 'input'],
|
||||||
|
input : ['onInput' , 'input'],
|
||||||
|
keydown : ['onKeydown' , 'input'],
|
||||||
|
click : ['onClickScope' , 'scope']
|
||||||
|
};
|
||||||
|
|
||||||
|
for( var e in events )
|
||||||
|
this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM events callbacks
|
||||||
|
*/
|
||||||
|
callbacks : {
|
||||||
|
onFocusBlur : function(e){
|
||||||
|
var text = e.target.value.trim();
|
||||||
|
|
||||||
|
if( e.type == "focus" )
|
||||||
|
e.target.className = 'input';
|
||||||
|
else if( e.type == "blur" && text == "" ){
|
||||||
|
e.target.className = 'input placeholder';
|
||||||
|
this.DOM.input.removeAttribute('style');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeydown : function(e){
|
||||||
|
var s = e.target.value;
|
||||||
|
if( e.key == "Backspace" && (s == "" || s.charCodeAt(0) == 8203) ){
|
||||||
|
this.removeTag( this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)').length - 1 );
|
||||||
|
}
|
||||||
|
if( e.key == "Escape" ){
|
||||||
|
e.target.value = '';
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
if( e.key == "Enter" ){
|
||||||
|
e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380
|
||||||
|
if( this.addTag(s) )
|
||||||
|
e.target.value = '';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput : function(e){
|
||||||
|
var value = e.target.value,
|
||||||
|
lastChar = value[value.length - 1];
|
||||||
|
|
||||||
|
e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';
|
||||||
|
|
||||||
|
if( value.indexOf(',') != -1 ){
|
||||||
|
this.addTag( value );
|
||||||
|
e.target.value = ''; // clear the input field's value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickScope : function(e){
|
||||||
|
if( e.target.tagName == "TAGS" )
|
||||||
|
this.DOM.input.focus();
|
||||||
|
if( e.target.tagName == "X" ){
|
||||||
|
this.removeTag( this.getNodeIndex(e.target.parentNode) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build tags suggestions using HTML datalist
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
buildDataList : function(){
|
||||||
|
var OPTIONS = "",
|
||||||
|
i,
|
||||||
|
datalist = "<datalist id='tagsSuggestions"+ this.id +"'> \
|
||||||
|
<label> \
|
||||||
|
select from the list: \
|
||||||
|
<select> \
|
||||||
|
<option value=''></option> \
|
||||||
|
[OPTIONS] \
|
||||||
|
</select> \
|
||||||
|
</label> \
|
||||||
|
</datalist>";
|
||||||
|
|
||||||
|
for( i=this.settings.whitelist.length; i--; )
|
||||||
|
OPTIONS += "<option>"+ this.settings.whitelist[i] +"</option>";
|
||||||
|
|
||||||
|
datalist = datalist.replace('[OPTIONS]', OPTIONS); // inject the options string in the right place
|
||||||
|
this.DOM.input.insertAdjacentHTML('afterend', datalist); // append the datalist HTML string in the Tags
|
||||||
|
|
||||||
|
return datalist;
|
||||||
|
},
|
||||||
|
|
||||||
|
getNodeIndex : function( node ){
|
||||||
|
var index = 0;
|
||||||
|
while( (node = node.previousSibling) )
|
||||||
|
if (node.nodeType != 3 || !/^\s*$/.test(node.data))
|
||||||
|
index++;
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
|
||||||
|
markTagByValue : function(value){
|
||||||
|
var tagIdx = this.value.findIndex(function(item){ return value.toLowerCase() === item.toLowerCase() }),
|
||||||
|
tag = this.DOM.scope.querySelectorAll('tag')[tagIdx];
|
||||||
|
|
||||||
|
if( tag ){
|
||||||
|
tag.classList.add('tagify--mark');
|
||||||
|
setTimeout(function(){ tag.classList.remove('tagify--mark') }, 2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make sure the tag, or words in it, is not in the blacklist
|
||||||
|
*/
|
||||||
|
isTagBlacklisted : function(v){
|
||||||
|
v = v.split(' ');
|
||||||
|
return this.settings.blacklist.filter(function(x){ return v.indexOf(x) != -1 }).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* make sure the tag, or words in it, is not in the blacklist
|
||||||
|
*/
|
||||||
|
isTagWhitelisted : function(v){
|
||||||
|
return this.settings.whitelist.indexOf(v) != -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTag : function( value ){
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.DOM.input.removeAttribute('style');
|
||||||
|
|
||||||
|
value = value.trim();
|
||||||
|
if( !value ) return;
|
||||||
|
|
||||||
|
return value.split(',').filter(function(v){ return !!v }).map(function(v){
|
||||||
|
var tagElm = document.createElement('tag');
|
||||||
|
v = v.trim();
|
||||||
|
|
||||||
|
if( !that.settings.duplicates && that.markTagByValue(v) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// check against blacklist & whitelist (if enforced)
|
||||||
|
if( that.isTagBlacklisted(v) || (that.settings.enforeWhitelist && !that.isTagWhitelisted(v)) ){
|
||||||
|
tagElm.classList.add('tagify--notAllowed');
|
||||||
|
setTimeout(function(){ that.removeTag(that.getNodeIndex(tagElm)) }, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the space below is important - http://stackoverflow.com/a/19668740/104380
|
||||||
|
tagElm.innerHTML = "<x></x><div><span title='"+ v +"'>"+ v +" </span></div>";
|
||||||
|
that.DOM.scope.insertBefore(tagElm, that.DOM.input.parentNode);
|
||||||
|
|
||||||
|
that.value.push(v);
|
||||||
|
that.update();
|
||||||
|
return tagElm;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTag : function( idx ){
|
||||||
|
var tagElm = this.DOM.scope.children[idx];
|
||||||
|
if( !tagElm) return;
|
||||||
|
|
||||||
|
tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px';
|
||||||
|
document.body.clientTop; // force repaint for the width to take affect before the "hide" class below
|
||||||
|
tagElm.classList.add('tagify--hide');
|
||||||
|
|
||||||
|
// manual timeout (hack, since transitionend cannot be used because of hover)
|
||||||
|
setTimeout(function(){
|
||||||
|
tagElm.parentNode.removeChild(tagElm);
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
this.value.splice(idx, 1);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
// update the origianl (hidden) input field's value
|
||||||
|
update : function(){
|
||||||
|
this.DOM.originalInput.value = this.value.join(', ');
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,9 @@ $newline never
|
|||||||
integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous">
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/tagify.css">
|
||||||
|
<script src="/static/js/tagify.js">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
||||||
^{pageHead pc}
|
^{pageHead pc}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<p>#{show $ submissionStamp $ submission}
|
^{view}
|
||||||
|
|
||||||
<form method=post action=@{EditSubmissionR submissionId}#form enctype=#{formEnctype}>
|
<form method=post action=@{EditSubmissionR submissionId}#form enctype=#{formEnctype}>
|
||||||
^{formWidget}
|
^{formWidget}
|
||||||
|
6
templates/edit-submission.julius
Normal file
6
templates/edit-submission.julius
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
var input = document.querySelector('input[data-role=tagsinput]'),
|
||||||
|
tagify = new Tagify( input, {
|
||||||
|
whitelist: #{tagsAvailableAsJSON},
|
||||||
|
autocomplete: true,
|
||||||
|
enforeWhitelist: true});
|
||||||
|
input.style.display = 'none';
|
Loading…
Reference in New Issue
Block a user