From: Vásáry Dániel Date: Wed, 29 Jan 2020 13:22:08 +0000 (+0000) Subject: git-tfs-id: [http://tfs.userrendszerhaz.hu:8080/tfs/DefaultCollection]$/MediaCube... X-Git-Url: http://git.useribm.hu/?a=commitdiff_plain;h=e430ae3339d2f852efd9d39279b115d1b51e5613;p=mediacube.git git-tfs-id: [tfs.userrendszerhaz.hu:8080/tfs/DefaultCollection]$/MediaCube;C31718 --- diff --git a/client/DxPlay/PlayerGraph.cs b/client/DxPlay/PlayerGraph.cs index 3cfa8619..88ed3a46 100644 --- a/client/DxPlay/PlayerGraph.cs +++ b/client/DxPlay/PlayerGraph.cs @@ -40,8 +40,8 @@ namespace DxPlay { EnableDeinterlace(videoDecoder); IBaseFilter sampleGrabber = AddSampleGrabber(graphBuilder, videoDecoder); SampleGrabber = (ISampleGrabber)sampleGrabber; - //IBaseFilter videoRenderer = AddRenderer(graphBuilder, sampleGrabber, handle); - IBaseFilter videoRenderer = AddSimpleRenderer(graphBuilder, sampleGrabber, handle); + IBaseFilter videoRenderer = AddRenderer(graphBuilder, sampleGrabber, handle); + //IBaseFilter videoRenderer = AddSimpleRenderer(graphBuilder, sampleGrabber, handle); FilterGraphTools.RenderPin(graphBuilder, sampleGrabber, "Output"); ConfigureSimpleRenderer(handle); diff --git a/client/MXFFileParser/MXFFile.cs b/client/MXFFileParser/MXFFile.cs index 0e98de2f..f01a562a 100644 --- a/client/MXFFileParser/MXFFile.cs +++ b/client/MXFFileParser/MXFFile.cs @@ -91,6 +91,11 @@ namespace Myriadbits.MXF { break; } } + //TODO XML kiolvasás és parse + public string InspectMetadata() { + ParsePartial(); + return null; + } /// /// Fully Parse an MXF file diff --git a/client/Maestro/Configuration/-UJ-configuration-tqc-check.json b/client/Maestro/Configuration/-UJ-configuration-tqc-check.json index 31499b3d..549dfcf6 100644 --- a/client/Maestro/Configuration/-UJ-configuration-tqc-check.json +++ b/client/Maestro/Configuration/-UJ-configuration-tqc-check.json @@ -1,6 +1,6 @@ { "title": "TQC check", - "active": true, + "active": false, "startInTray": false, "enableCustomMetadataId": true, "player": { diff --git a/client/Maestro/Configuration/editor.json b/client/Maestro/Configuration/editor.json index 2024a0e4..0e802bf1 100644 --- a/client/Maestro/Configuration/editor.json +++ b/client/Maestro/Configuration/editor.json @@ -12,15 +12,15 @@ "$type": "UNCSource", "filter": "avi,wav,mxf,mts", "local": { - "address": "file://c:/remote/promise/FINISHED_SHOWS", + "address": "file://c:/_video/", "timeout": 1000 }, - "remote": { - "address": "ftp://10.11.1.100/Promise/FINISHED_SHOWS", - "userName": "editor1", - "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", - "timeout": 1000 - } + "remote": { + "address": "ftp://10.11.1.100/Promise/", + "userName": "editor1", + "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", + "timeout": 1000 + } }, "metadatas": [ { @@ -47,28 +47,39 @@ "label": "Adáskész", "processor": "FXPTargetProcessor", "outputFormat": "%ID%", + "reference": [ "Mentés" ], "saveSegments": true, + "subFolderFormat": "%SOURCEFOLDERNAME%", "tag": "Adáskész", - "remote": { - "address": "ftp://10.11.1.100/Promise/PROGRAM/TEST", - "userName": "editor1", - "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", - "timeout": 1000 - } + "remote": { + "address": "ftp://10.11.1.100/Promise/PROGRAM/TEST", + "userName": "editor1", + "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", + "timeout": 1000 + } }, - { - "label": "Archiválás", - "processor": "FXPTargetProcessor", - "outputFormat": "%ID%", - "saveArchiveMetadata": true, - "tag": "Archiválás", - "remote": { - "address": "ftp://10.11.1.100/Promise/ARCHIVE/TEST", - "userName": "editor1", - "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", - "timeout": 1000 - } - } + { + "label": "Archiválás", + "processor": "FXPTargetProcessor", + "outputFormat": "%ID%", + "saveArchiveMetadata": true, + "tag": "Archiválás", + "remote": { + "address": "ftp://10.11.1.100/Promise/ARCHIVE/TEST", + "userName": "editor1", + "password": "mBsAKn0RRr+lErAWAu+oMD/3CRxlBLNvm3UB84SKl5KBVYD5+wIANFL0eszfbAUtzYKqdN/dEB/6ItBNz9D6C4/hppcYrg0+73+xFW9KYEwd2KfgHaH5uslbA/8IyI/U", + "timeout": 1000 + } + }, + { + "label": "Mentés", + "readOnly": true, + "processor": "UNCTargetProcessor", + "outputFormat": "%SOURCENAME%", + "tag": "Mentés", + "subFolderFormat": "Transfered", + "moveToFolder": true + } ] } diff --git a/client/Maestro/MaestroForm.Metadata.cs b/client/Maestro/MaestroForm.Metadata.cs index e3b0c4a3..7a7babaa 100644 --- a/client/Maestro/MaestroForm.Metadata.cs +++ b/client/Maestro/MaestroForm.Metadata.cs @@ -342,10 +342,10 @@ namespace Maestro { IEnumerable octopusResult = null; switch (metadataType) { case MetadataType.OctopusPlaceHolder: - octopusResult = api.GetStoriesByPlaceHolderID(id); + octopusResult = api?.GetStoriesByPlaceHolderID(id); break; case MetadataType.OctopusStory: - octopusResult = api.GetStoriesByParentStoryID(id); + octopusResult = api?.GetStoriesByParentStoryID(id); break; } List stories = octopusResult?.ToList(); diff --git a/client/Maestro/MaestroForm.Target.cs b/client/Maestro/MaestroForm.Target.cs index f4d301c2..8332eb8d 100644 --- a/client/Maestro/MaestroForm.Target.cs +++ b/client/Maestro/MaestroForm.Target.cs @@ -41,6 +41,7 @@ namespace Maestro { Dock = DockStyle.Top, Tag = target }; + checkBox.Enabled = !target.ReadOnly; formTooltip.SetToolTip(checkBox, target.Remote?.Address?.ToString()); checkBox.CheckStateChanged += (s, e) => OnChecked(checkBox, target); panelActions.Controls.Add(checkBox); @@ -192,8 +193,10 @@ namespace Maestro { //szegmens informaciook hozzaadasa if (ArchiveMetadata != null && HasNoneEmptySegments()) { var segments = "Szegmensek:" + Environment.NewLine; - MovieSegments.ToList().ForEach(s => { segments += $"{s.TCIn.ToString()} - {s.TCOut.ToString()}" + Environment.NewLine; }); - ArchiveMetadata.mediaDescription += Environment.NewLine + segments; + if (MovieSegments != null && MovieSegments.Count > 0) { + MovieSegments.ToList().ForEach(s => { segments += $"{s.TCIn.ToString()} - {s.TCOut.ToString()}" + Environment.NewLine; }); + ArchiveMetadata.mediaDescription += Environment.NewLine + segments; + } } return true; @@ -203,6 +206,9 @@ namespace Maestro { FileSystemSource source = bindingSource.DataSource as FileSystemSource; TargetProcessorParameter result = new TargetProcessorParameter(); + if (sourceItem is FileSourceItem) { + result.SourcePath = ((FileSourceItem)sourceItem).FileInfo.DirectoryName; + } result.SourcePathOverride = (source == null || source.Path.Equals(Configuration.Source.Local.Address.LocalPath)) ? null : source.Path; result.MediaCubeApi = mediaCubeApi; result.TrafficApi = trafficIDSelector.trafficAPI; @@ -228,32 +234,46 @@ namespace Maestro { result.ArchiveMetadata.duration = sourceItem.Frames; result.ArchiveMetadata.userName = result.UserName; if (SelectedMetadata.Kind == MetadataType.MediaCube) { - result.ArchiveMetadata.itemHouseId = PatternNameMaker.Get(result.ArchiveMetadata.itemHouseId, result.ID, result.InputFileName, null, null, result.MetadataText); - result.ArchiveMetadata.itemTitle = PatternNameMaker.Get(result.ArchiveMetadata.itemTitle, result.ID, result.InputFileName, null, null, result.MetadataText); - result.ArchiveMetadata.mediaHouseId = PatternNameMaker.Get(result.ArchiveMetadata.mediaHouseId, result.ID, result.InputFileName, null, null, result.MetadataText); - result.ArchiveMetadata.mediaTitle = PatternNameMaker.Get(result.ArchiveMetadata.mediaTitle, result.ID, result.InputFileName, null, null, result.MetadataText); + result.ArchiveMetadata.itemHouseId = PatternNameMaker.Get(result.ArchiveMetadata.itemHouseId, result.ID, null, result.InputFileName, null, null, result.MetadataText); + result.ArchiveMetadata.itemTitle = PatternNameMaker.Get(result.ArchiveMetadata.itemTitle, result.ID, null, result.InputFileName, null, null, result.MetadataText); + result.ArchiveMetadata.mediaHouseId = PatternNameMaker.Get(result.ArchiveMetadata.mediaHouseId, result.ID, null, result.InputFileName, null, null, result.MetadataText); + result.ArchiveMetadata.mediaTitle = PatternNameMaker.Get(result.ArchiveMetadata.mediaTitle, result.ID, null, result.InputFileName, null, null, result.MetadataText); } } return result; } - private List HandleCheckBoxReferences(string[] reference, bool check) { + private bool HasCheckedReferer(CheckBox checkBox) { + var controlls = panelActions.Controls; + foreach (Control actual in controlls) { + if (actual is CheckBox actualCheckbox) { + Target target = (Target)actualCheckbox.Tag; + if (target.Reference!= null && target.Reference.Contains(checkBox.Text) && actualCheckbox.Checked) + return true; + } + } + return false; + } + + private void HandleCheckBoxReferences(string[] reference, bool check) { if (reference == null || reference.Length == 0) - return null; + return; var controlls = panelActions.Controls; - List result = null; foreach (Control actual in controlls) { if (actual is CheckBox actualCheckbox && reference.Contains(actualCheckbox.Text)) { if (check) { actualCheckbox.Checked = check; - if (result == null) - result = new List(); - result.Add(actualCheckbox); + } else { + //minden parent unchecked? + if (!HasCheckedReferer(actualCheckbox)) + actualCheckbox.Checked = check; } - actualCheckbox.Enabled = !check; + + Target target = (Target)actualCheckbox.Tag; + if (!target.ReadOnly) + actualCheckbox.Enabled = !check; } } - return result; } private ISourceItem GetSourceItemFromBindingSource(string actual) { @@ -267,8 +287,11 @@ namespace Maestro { private void ChangeProcessButtonsState(bool enabled) { if (panelActions.Controls == null) return; - foreach (Control c in panelActions.Controls) - c.Enabled = enabled; + foreach (Control c in panelActions.Controls) { + Target target = (Target)c.Tag; + if (!target.ReadOnly) + c.Enabled = enabled; + } } private void UpdateProcessorButtonsEnabled() { diff --git a/client/Maestro/Properties/AssemblyInfo.cs b/client/Maestro/Properties/AssemblyInfo.cs index c29e19c2..0d15fbc5 100644 --- a/client/Maestro/Properties/AssemblyInfo.cs +++ b/client/Maestro/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.1.0.0")] -[assembly: AssemblyFileVersion("2.1.0.0")] +[assembly: AssemblyVersion("2.1.0.4")] +[assembly: AssemblyFileVersion("2.1.0.4")] diff --git a/client/Maestro/RestoreMedia.cs b/client/Maestro/RestoreMedia.cs index 08ac6228..45591277 100644 --- a/client/Maestro/RestoreMedia.cs +++ b/client/Maestro/RestoreMedia.cs @@ -45,7 +45,7 @@ namespace Maestro { })); }); api = new MediaCubeWSApi(mediaCubeMetadata.WSServer, messageBus); - string pattern = PatternNameMaker.Get(mediaCubeMetadata.RestoreNamePattern, null, null, null, null, null, null); + string pattern = PatternNameMaker.Get(mediaCubeMetadata.RestoreNamePattern, null, null, null, null, null, null, null); JObject data = new JObject { { "targetPath", mediaCubeMetadata.ServerRestoreFolder }, { "targetNamePattern", pattern }, diff --git a/client/MaestroShared/Commons/PatternNameMaker.cs b/client/MaestroShared/Commons/PatternNameMaker.cs index edd42ee9..16e268a3 100644 --- a/client/MaestroShared/Commons/PatternNameMaker.cs +++ b/client/MaestroShared/Commons/PatternNameMaker.cs @@ -19,6 +19,7 @@ namespace MaestroShared.Commons { private const string PATTERN_IDROOT = "%IDROOT%"; private const string PATTERN_GUID = "%GUID%"; private const string PATTERN_SOURCENAME = "%SOURCENAME%"; + private const string PATTERN_SOURCEFOLDERNAME = "%SOURCEFOLDERNAME%"; private const string PATTERN_SOURCESTARTID = "%SOURCESTARTID%"; private const string PATTERN_TIMESTAMP = "%TIMESTAMP%"; private const string PATTERN_DATESTAMP = "%DATESTAMP%"; @@ -45,7 +46,7 @@ namespace MaestroShared.Commons { return result; } - static public string Get(string pattern, string id, string inputName, string outputName, string userName, string text, DateTime? storedDateTime = null, string json = null, string itemTitle = null, string mediaTitle = null, string format = null) { + static public string Get(string pattern, string id, string sourcePath, string inputName, string outputName, string userName, string text, DateTime? storedDateTime = null, string json = null, string itemTitle = null, string mediaTitle = null, string format = null) { if (pattern == null) return null; string idRoot = id != null && id.Contains(UNDERSCORE) ? id.Split(UNDERSCORE[0])[0] : id; @@ -72,6 +73,10 @@ namespace MaestroShared.Commons { result = result.Replace(PATTERN_SOURCESTARTID, sourceStartID); } + if (!String.IsNullOrEmpty(sourcePath)) { + result = result.Replace(PATTERN_SOURCEFOLDERNAME, Normalize(Path.GetFileName(sourcePath))); + } + if (!String.IsNullOrEmpty(outputName)) result = result.Replace(PATTERN_TARGETNAME, outputName); diff --git a/client/MaestroShared/Configuration/ConfigurationInfo.cs b/client/MaestroShared/Configuration/ConfigurationInfo.cs index 825a4c8c..800f58b4 100644 --- a/client/MaestroShared/Configuration/ConfigurationInfo.cs +++ b/client/MaestroShared/Configuration/ConfigurationInfo.cs @@ -95,6 +95,7 @@ namespace MaestroShared.Configuration { } public class Target { + public bool ReadOnly { get; set; } public string Label { get; set; } public string Processor { get; set; } public string OutputFormat { get; set; } @@ -125,6 +126,8 @@ namespace MaestroShared.Configuration { public string PopupMessage { get; set; } public string SourceNexioAgency { get; set; } public int SourceNexioKillDateDays { get; set; } + public bool MoveToFolder { get; set; } + } public class Connection { diff --git a/client/MaestroShared/Properties/AssemblyInfo.cs b/client/MaestroShared/Properties/AssemblyInfo.cs index f3891638..5936c1a5 100644 --- a/client/MaestroShared/Properties/AssemblyInfo.cs +++ b/client/MaestroShared/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.9.8")] -[assembly: AssemblyFileVersion("2.0.9.8")] +[assembly: AssemblyVersion("2.1.0.1")] +[assembly: AssemblyFileVersion("2.1.0.1")] diff --git a/client/MaestroShared/Targets/TargetProcessorParameter.cs b/client/MaestroShared/Targets/TargetProcessorParameter.cs index 932d3e05..c772f970 100644 --- a/client/MaestroShared/Targets/TargetProcessorParameter.cs +++ b/client/MaestroShared/Targets/TargetProcessorParameter.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; namespace MaestroShared.Target { public class TargetProcessorParameter { + public string SourcePath { get; set; } public string SourcePathOverride { get; set; } public Source SourceConfig { get; set; } public Configuration.Target TargetConfig { get; set; } diff --git a/client/MaestroShared/Targets/UNCTargetProcessor.cs b/client/MaestroShared/Targets/UNCTargetProcessor.cs index b150fc17..39896b0d 100644 --- a/client/MaestroShared/Targets/UNCTargetProcessor.cs +++ b/client/MaestroShared/Targets/UNCTargetProcessor.cs @@ -40,6 +40,7 @@ namespace MaestroShared.Targets { private const string STAR = "*"; protected FileInfo inputFile; protected string workingDir; + private string sourcePath; public WorkflowAction workFlowAction { get; set; } @@ -47,10 +48,15 @@ namespace MaestroShared.Targets { logger.Trace(Strings.ENTRY); base.Initialize(parent, parameters); InputName = parameters.InputFileName; - if (!String.IsNullOrEmpty(parameters.SourcePathOverride)) + sourcePath = parameters.SourcePath; + if (!string.IsNullOrEmpty(parameters.SourcePathOverride)) Input = Path.Combine(parameters.SourcePathOverride, parameters.InputFileName); - else - Input = Path.Combine(parameters.SourceConfig.Local.Address.LocalPath, parameters.InputFileName); + else { + if (parameters.SourcePath == null) + Input = Path.Combine(parameters.SourceConfig.Local.Address.LocalPath, parameters.InputFileName); + else + Input = Path.Combine(parameters.SourcePath, parameters.InputFileName); + } inputFile = new FileInfo(Input); ID = parameters.ID; workFlowAction = new WorkflowAction() { @@ -73,8 +79,9 @@ namespace MaestroShared.Targets { if (Parameters.TargetConfig.SaveSegments && Parameters.MovieSegments != null) { string fileName = Parameters?.TrafficApi?.SaveSegments(Parameters.VariantID, Parameters.MetadataKind, Parameters.MovieSegments, Parameters.SelectedSegments); if (true.Equals(Parameters?.TrafficMetadata?.MultiSegmentEnabled)) { - //a Traffic adja a nevet - OutputName = fileName ?? throw new Exception("A fájlnév nem lehet üres."); + + OutputName = fileName + inputFile.Extension; + } } } @@ -84,7 +91,7 @@ namespace MaestroShared.Targets { bool result = false; try { BeforeExecute(); - workingDir = DetermineWorkingDirectory(Parameters.TargetConfig.Remote); + workingDir = DetermineWorkingDirectory(Parameters.TargetConfig); EnsureDirectoryExistence(workingDir); //multiszegmens mukodes eseten a filenevet a traffic generalja @@ -103,7 +110,7 @@ namespace MaestroShared.Targets { UploadFile(); ValidateTransfer(); //logger.Info("Spend (s):" + (DateTime.Now - started).TotalSeconds); - if (Parameters.TargetConfig.DeleteAfterCopy) + if (Parameters.TargetConfig.DeleteAfterCopy || Parameters.TargetConfig.MoveToFolder) DeleteAfterCopy(); ExecuteCompleted(); @@ -217,6 +224,7 @@ namespace MaestroShared.Targets { } if (Parameters.TargetConfig.SendEmailOnSuccess && !String.IsNullOrEmpty(Parameters.TargetConfig.SuccessEmailRecipient) && !String.IsNullOrEmpty(Parameters.TargetConfig.SuccessEmailPattern)) SendEmail(Parameters.TargetConfig.SuccessEmailRecipient, Parameters.TargetConfig.SuccessEmailSubject, Parameters.TargetConfig.SuccessEmailPattern); + logger.Trace(Strings.EXIT); } @@ -319,9 +327,8 @@ namespace MaestroShared.Targets { return path; } - protected string DetermineWorkingDirectory(Connection connection) { - logger.Trace(Strings.ENTRY); - string result = Slash(connection.Address.LocalPath); + private string ApplyDynamicOutput(string outputPath) { + string result = outputPath; if (String.IsNullOrEmpty(Parameters.TargetConfig.SubFolderFormat)) return result; string subFolderName = Slash(GetDynamicText(Parameters.TargetConfig.SubFolderFormat)); @@ -340,7 +347,19 @@ namespace MaestroShared.Targets { else result = Slash(Path.Combine(result, searchResult)); } + return result; + } + protected string DetermineWorkingDirectory(Configuration.Target target) { + logger.Trace(Strings.ENTRY); + string result = null; + Connection connection = target.Remote; + if (target.MoveToFolder) { + result = sourcePath; + } else { + result = Slash(connection.Address.LocalPath); + } + result = ApplyDynamicOutput(result); logger.Trace(Strings.EXIT); return result; } @@ -422,7 +441,7 @@ namespace MaestroShared.Targets { } protected string GetDynamicText(string pattern) { - return PatternNameMaker.Get(pattern, ID, InputName, Output, Parameters.UserName, Parameters.MetadataText, Parameters.CreateDate, Parameters.ArchiveMetadata?.ToString(), Parameters.ArchiveMetadata?.itemTitle, Parameters.ArchiveMetadata?.mediaTitle, Parameters.ArchiveMetadata?.format); + return PatternNameMaker.Get(pattern, ID, sourcePath, InputName, Output, Parameters.UserName, Parameters.MetadataText, Parameters.CreateDate, Parameters.ArchiveMetadata?.ToString(), Parameters.ArchiveMetadata?.itemTitle, Parameters.ArchiveMetadata?.mediaTitle, Parameters.ArchiveMetadata?.format); } private string CreateOutputFileName() { diff --git a/server/-configuration/log4j2.xml b/server/-configuration/log4j2.xml index 1be0d20a..dbc3d3bc 100644 --- a/server/-configuration/log4j2.xml +++ b/server/-configuration/log4j2.xml @@ -55,7 +55,7 @@ - + diff --git a/server/-modules/MediaCube.iml b/server/-modules/MediaCube.iml new file mode 100644 index 00000000..36ec37c1 --- /dev/null +++ b/server/-modules/MediaCube.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/server/-product/user.jobengine.product.iml b/server/-product/user.jobengine.product.iml new file mode 100644 index 00000000..36ec37c1 --- /dev/null +++ b/server/-product/user.jobengine.product.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/server/hu.user.mediacube.executors.tests/META-INF/MANIFEST.MF b/server/hu.user.mediacube.executors.tests/META-INF/MANIFEST.MF index 2fa649d6..bba0565e 100644 --- a/server/hu.user.mediacube.executors.tests/META-INF/MANIFEST.MF +++ b/server/hu.user.mediacube.executors.tests/META-INF/MANIFEST.MF @@ -6,4 +6,5 @@ Bundle-Version: 1.0.0.qualifier Fragment-Host: user.jobengine.executors;bundle-version="1.0.0" Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: org.apache.commons.io.filefilter;version="2.2.0", + org.apache.commons.io.output;version="2.2.0", org.junit diff --git a/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/HSMMigrateStepTest.java b/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/HSMMigrateStepTest.java index 44c216d5..6c083e0c 100644 --- a/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/HSMMigrateStepTest.java +++ b/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/HSMMigrateStepTest.java @@ -75,7 +75,7 @@ public class HSMMigrateStepTest { public void testResumableCopy() throws Exception { HSMMigrateStep sut = new HSMMigrateStep(); Paths.get("c:/_video/03c.mp4").toFile().delete(); - sut.copyChunk(Paths.get("c:/_video/1.txt"), Paths.get("c:/_video/2.txt"), 5); + //sut.copyChunk(Paths.get("c:/_video/1.txt"), Paths.get("c:/_video/2.txt"), 5); sut.resumeableCopy(Paths.get("c:/_video/1.txt"), Paths.get("c:/_video/2.txt")); } } diff --git a/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/MediaBaseTest.java b/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/MediaBaseTest.java new file mode 100644 index 00000000..5d5debb0 --- /dev/null +++ b/server/hu.user.mediacube.executors.tests/src/hu/user/mediacube/executors/tests/MediaBaseTest.java @@ -0,0 +1,53 @@ +package hu.user.mediacube.executors.tests; + +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.net.ftp.FTPClient; +import org.junit.Test; + +import user.commons.RemoteFile; +import user.commons.StoreUri; +import user.commons.remotestore.FtpDirectoryLister; +import user.commons.remotestore.IDirectoryLister; +import user.commons.remotestore.RemoteStoreProtocol; + +public class MediaBaseTest { + + @Test + public void listMediaBase() throws Exception { + StoreUri nexioUri = new StoreUri(); + nexioUri.setProtocol(RemoteStoreProtocol.FTP); + nexioUri.setUri("10.10.1.55"); + nexioUri.setPortNumber(2098); + nexioUri.setUserName("ftp"); + nexioUri.setPassword("ftp"); + try { + FTPClient ftp = ((FtpDirectoryLister) nexioUri.getLister()).connect(); + IDirectoryLister lister = nexioUri.getLister(); + List list = lister.list(); + for (RemoteFile rf : list) { + if (rf.getIsFolder()) + continue; + String baseName = FilenameUtils.getBaseName(rf.getName()); + + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + ftp.retrieveFile(baseName + ".xml", output); + byte[] targetArray = output.toByteArray(); + System.out.println(rf.getName() + " " + targetArray.length); + //Thread.sleep(100); + } catch (Exception ie) { + System.err.println(ie.getMessage()); + } + } + + } catch (Exception e) { + System.err.println(e.getMessage()); + } finally { + nexioUri.cleanUp(); + } + + } + +} diff --git a/server/hu.user.mediacube.indexer/indexer (1).iml b/server/hu.user.mediacube.indexer/indexer (1).iml new file mode 100644 index 00000000..a643bf92 --- /dev/null +++ b/server/hu.user.mediacube.indexer/indexer (1).iml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/hu.user.mediacube.indexer/indexer (2).iml b/server/hu.user.mediacube.indexer/indexer (2).iml new file mode 100644 index 00000000..75311229 --- /dev/null +++ b/server/hu.user.mediacube.indexer/indexer (2).iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/hu.user.mediacube.indexer/indexer.iml b/server/hu.user.mediacube.indexer/indexer.iml new file mode 100644 index 00000000..3b8b11c2 --- /dev/null +++ b/server/hu.user.mediacube.indexer/indexer.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/test/.classpath b/server/test/.classpath new file mode 100644 index 00000000..63b7e892 --- /dev/null +++ b/server/test/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/server/test/.project b/server/test/.project new file mode 100644 index 00000000..c8a61fe0 --- /dev/null +++ b/server/test/.project @@ -0,0 +1,17 @@ + + + test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/server/test/.settings/org.eclipse.jdt.core.prefs b/server/test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..bb35fa0a --- /dev/null +++ b/server/test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/server/test/src/Echo.java b/server/test/src/Echo.java new file mode 100644 index 00000000..2d12d028 --- /dev/null +++ b/server/test/src/Echo.java @@ -0,0 +1,11 @@ + +import java.io.IOException; + +public class Echo { + + public static void main(String[] args) throws IOException { + System.out.println("Hello boot. Press enter!"); + System.in.read(); + } + +} diff --git a/server/user.commons.log4j2/user.commons.log4j2.iml b/server/user.commons.log4j2/user.commons.log4j2.iml new file mode 100644 index 00000000..ebab1e58 --- /dev/null +++ b/server/user.commons.log4j2/user.commons.log4j2.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/user.jobengine.executors/src/user/jobengine/server/steps/HSMMigrateStep.java b/server/user.jobengine.executors/src/user/jobengine/server/steps/HSMMigrateStep.java index ed6fc097..6c8f3cf5 100644 --- a/server/user.jobengine.executors/src/user/jobengine/server/steps/HSMMigrateStep.java +++ b/server/user.jobengine.executors/src/user/jobengine/server/steps/HSMMigrateStep.java @@ -66,33 +66,6 @@ public class HSMMigrateStep extends JobStep { volumeHistory.drop(); } - public void copyChunk(Path source, Path target, long chunk) throws IOException { - - File sourceFile = source.toFile(); - File targetFile = target.toFile(); - - try (InputStream in = new BufferedInputStream(new FileInputStream(sourceFile)); - OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile))) { - - byte[] buffer = new byte[1]; - long copied = 0; - int lengthRead; - while ((lengthRead = in.read(buffer)) > 0) { - out.write(buffer, 0, lengthRead); - out.flush(); - - copied += lengthRead; - if (copied > chunk) { - out.close(); - in.close(); - return; - } - - } - } - - } - private BasicDBObject createMetadata(String volumeName, String fileName) throws Exception { Path filePath = Paths.get(fileName); @@ -297,6 +270,8 @@ public class HSMMigrateStep extends JobStep { repeat = 0; successCopy = true; } catch (Exception e) { + if (Files.exists(targetFilePath) && targetFilePath.toFile().length() == 0) + Files.delete(targetFilePath); //logger.warn(marker, "Hiba a másolás során: {} ({})", sourceFilePath, e.getMessage()); repeat--; } @@ -363,7 +338,7 @@ public class HSMMigrateStep extends JobStep { try (InputStream in = new BufferedInputStream(new FileInputStream(sourceFile)); OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile, targetExists))) { - byte[] buffer = new byte[256 * 1024 * 4 * 100]; + byte[] buffer = new byte[128 * 1024]; int lengthRead; if (targetExists) diff --git a/server/user.jobengine.executors/user.jobengine.executors.iml b/server/user.jobengine.executors/user.jobengine.executors.iml new file mode 100644 index 00000000..4aa9e801 --- /dev/null +++ b/server/user.jobengine.executors/user.jobengine.executors.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/user.jobengine.osgi.commons/user.jobengine.osgi.commons.iml b/server/user.jobengine.osgi.commons/user.jobengine.osgi.commons.iml new file mode 100644 index 00000000..282fc1a8 --- /dev/null +++ b/server/user.jobengine.osgi.commons/user.jobengine.osgi.commons.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/user.jobengine.osgi.db/user.jobengine.osgi.db.iml b/server/user.jobengine.osgi.db/user.jobengine.osgi.db.iml new file mode 100644 index 00000000..cf7fe4ba --- /dev/null +++ b/server/user.jobengine.osgi.db/user.jobengine.osgi.db.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/user.jobengine.osgi.server/META-INF/MANIFEST.MF b/server/user.jobengine.osgi.server/META-INF/MANIFEST.MF index c0fcecad..b51a2ed5 100644 --- a/server/user.jobengine.osgi.server/META-INF/MANIFEST.MF +++ b/server/user.jobengine.osgi.server/META-INF/MANIFEST.MF @@ -6,6 +6,7 @@ Bundle-Version: 1.0.0 Service-Component: OSGI-INF/component.xml, OSGI-INF/componentBinder.xml Import-Package: javax.servlet;version="3.1.0", javax.servlet.http;version="3.1.0", + org.apache.commons.io.output;version="2.2.0", org.apache.logging.log4j;version="2.8.2", org.apache.logging.log4j.message;version="2.8.2", org.eclipse.core.runtime.adaptor, diff --git a/server/user.jobengine.osgi.server/css/tagify.css b/server/user.jobengine.osgi.server/css/tagify.css new file mode 100644 index 00000000..b4f589b0 --- /dev/null +++ b/server/user.jobengine.osgi.server/css/tagify.css @@ -0,0 +1 @@ +:root{--tagify-dd-color-primary:rgb(53,149,246);--tagify-dd-bg-color:white}.tagify{--tags-border-color:#DDD;--tag-bg:#E5E5E5;--tag-hover:#D3E2E2;--tag-text-color:black;--tag-text-color--edit:black;--tag-pad:0.3em 0.5em;--tag-inset-shadow-size:1.1em;--tag-invalid-color:#D39494;--tag-invalid-bg:rgba(211, 148, 148, 0.5);--tag-remove-bg:rgba(211, 148, 148, 0.3);--tag-remove-btn-bg:none;--tag-remove-btn-bg--hover:#c77777;--tag--min-width:1ch;--tag--max-width:auto;--tag-hide-transition:.3s;--loader-size:.8em;display:flex;align-items:flex-start;flex-wrap:wrap;border:1px solid #ddd;border:1px solid var(--tags-border-color);padding:0;line-height:1.1;cursor:text;outline:0;position:relative;transition:.1s}@keyframes tags--bump{30%{transform:scale(1.2)}}@keyframes rotateLoader{to{transform:rotate(1turn)}}.tagify:hover{border-color:#ccc}.tagify.tagify--focus{transition:0s;border-color:#3595f6}.tagify[readonly]{cursor:default}.tagify[readonly]>.tagify__input{visibility:hidden;width:0;margin:5px 0}.tagify[readonly] .tagify__tag__removeBtn{display:none}.tagify[readonly] .tagify__tag>div{padding:.3em .5em;padding:var(--tag-pad)}.tagify[readonly] .tagify__tag>div::before{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}.tagify--loading .tagify__input::before{content:none}.tagify--loading .tagify__input::after{content:'';vertical-align:middle;margin:-2px 0 -2px .5em;opacity:1;width:.7em;height:.7em;width:var(--loader-size);height:var(--loader-size);border:3px solid;border-color:#eee #bbb #888 transparent;border-radius:50%;animation:rotateLoader .4s infinite linear}.tagify--loading .tagify__input:empty::after{margin-left:0}.tagify+input,.tagify+textarea{display:none!important}.tagify__tag{display:inline-flex;align-items:center;margin:5px 0 5px 5px;position:relative;z-index:1;outline:0;cursor:default;transition:.13s ease-out}.tagify__tag>div{vertical-align:top;box-sizing:border-box;max-width:100%;padding:.3em .5em;padding:var(--tag-pad);color:#000;color:var(--tag-text-color);line-height:inherit;border-radius:3px;-webkit-user-select:none;user-select:none;transition:.13s ease-out}.tagify__tag>div>*{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:top;min-width:var(--tag--min-width);max-width:var(--tag--max-width);transition:.8s ease,.1s color}.tagify__tag>div>[contenteditable]{outline:0;-webkit-user-select:text;user-select:text;cursor:text;margin:-2px;padding:2px;max-width:350px}.tagify__tag>div::before{content:'';position:absolute;border-radius:inherit;left:0;top:0;right:0;bottom:0;z-index:-1;pointer-events:none;transition:120ms ease;animation:tags--bump .3s ease-out 1;box-shadow:0 0 0 1.1em #e5e5e5 inset;box-shadow:0 0 0 calc(var(--tag-inset-shadow-size)) var(--tag-bg) inset}.tagify__tag:hover:not([readonly]) div::before{top:-2px;right:-2px;bottom:-2px;left:-2px;box-shadow:0 0 0 1.1em #d3e2e2 inset;box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-hover) inset}.tagify__tag.tagify--noAnim{animation:none}.tagify__tag.tagify--hide{width:0!important;padding-left:0;padding-right:0;margin-left:0;margin-right:0;opacity:0;transform:scale(0);transition:.3s;transition:var(--tag-hide-transition);pointer-events:none}.tagify__tag.tagify--mark div::before{animation:none}.tagify__tag.tagify--notAllowed div>span{opacity:.5}.tagify__tag.tagify--notAllowed div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.5) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-invalid-bg) inset!important;transition:.2s}.tagify__tag[readonly] .tagify__tag__removeBtn{display:none}.tagify__tag[readonly]>div::before{background:linear-gradient(45deg,var(--tag-bg) 25%,transparent 25%,transparent 50%,var(--tag-bg) 50%,var(--tag-bg) 75%,transparent 75%,transparent) 0/5px 5px;box-shadow:none;filter:brightness(.95)}.tagify__tag--editable>div{color:#000;color:var(--tag-text-color--edit)}.tagify__tag--editable>div::before{box-shadow:0 0 0 2px #d3e2e2 inset!important;box-shadow:0 0 0 2px var(--tag-hover) inset!important}.tagify__tag--editable.tagify--invalid>div::before{box-shadow:0 0 0 2px #d39494 inset!important;box-shadow:0 0 0 2px var(--tag-invalid-color) inset!important}.tagify__tag__removeBtn{order:5;display:inline-flex;align-items:center;justify-content:center;border-radius:50px;cursor:pointer;font:14px Serif;background:0 0;background:var(--tag-remove-btn-bg);color:#000;color:var(--tag-text-color);width:14px;height:14px;margin-right:4.66667px;margin-left:-4.66667px;transition:.2s ease-out}.tagify__tag__removeBtn::after{content:"\00D7"}.tagify__tag__removeBtn:hover{color:#fff;background:#c77777;background:var(--tag-remove-btn-bg--hover)}.tagify__tag__removeBtn:hover+div>span{opacity:.5}.tagify__tag__removeBtn:hover+div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.3) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg) inset!important;transition:.2s}.tagify:not(.tagify--mix) .tagify__input br{display:none}.tagify:not(.tagify--mix) .tagify__input *{display:inline;white-space:nowrap}.tagify__input{display:block;min-width:110px;margin:5px;padding:.3em .5em;padding:var(--tag-pad,.3em .5em);line-height:inherit;position:relative;white-space:pre-line}.tagify__input::before{display:inline-block;width:0}.tagify__input:empty::before{transition:.2s ease-out;opacity:.5;transform:none;width:auto}.tagify__input:focus{outline:0}.tagify__input:focus::before{transition:.2s ease-out;opacity:0;transform:translatex(6px)}@supports (-moz-appearance:none){.tagify__input:focus::before{display:none}}.tagify__input:focus:empty::before{transition:.2s ease-out;opacity:.3;transform:none}@supports (-moz-appearance:none){.tagify__input:focus:empty::before{display:inline-block}}.tagify__input::before{content:attr(data-placeholder);line-height:1.8;position:absolute;top:0;z-index:1;color:#000;white-space:nowrap;pointer-events:none;opacity:0}.tagify--mix .tagify__input::before{position:static;line-height:inherit}@supports (-moz-appearance:none){.tagify__input::before{line-height:inherit;position:relative}}.tagify__input::after{content:attr(data-suggest);display:inline-block;white-space:pre;color:#000;opacity:.3;pointer-events:none;max-width:100px}.tagify__input .tagify__tag{margin:0}.tagify__input .tagify__tag>div{padding-top:0;padding-bottom:0}.tagify--mix{line-height:1.7}.tagify--mix .tagify__input{padding:5px;margin:0;width:100%;height:100%;line-height:inherit}.tagify--mix .tagify__input::after{content:none}.tagify--select::after{content:'>';opacity:.5;position:absolute;top:50%;right:0;bottom:0;font:16px monospace;line-height:8px;height:8px;pointer-events:none;transform:translate(-150%,-50%) scaleX(1.2) rotate(90deg);transition:.2s ease-in-out}.tagify--select[aria-expanded=true]::after{transform:translate(-150%,-50%) rotate(270deg) scaleY(1.2)}.tagify--select .tagify__tag{position:absolute;top:0;right:1.8em;bottom:0}.tagify--select .tagify__tag div{display:none}.tagify--select .tagify__input{width:100%}.tagify--invalid{--tags-border-color:#D39494}.tagify__dropdown{position:absolute;z-index:9999;transform:translateY(1px);overflow:hidden}.tagify__dropdown[placement=top]{margin-top:0;transform:translateY(-2px)}.tagify__dropdown[placement=top] .tagify__dropdown__wrapper{border-top-width:1px;border-bottom-width:0}.tagify__dropdown--text{box-shadow:0 0 0 3px rgba(var(--tagify-dd-color-primary),.1);font-size:.9em}.tagify__dropdown--text .tagify__dropdown__wrapper{border-width:1px}.tagify__dropdown__wrapper{max-height:300px;overflow:hidden;background:#fff;background:var(--tagify-dd-bg-color);border:1px solid #3595f6;border-color:var(--tagify-dd-color-primary);border-top-width:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.2);transition:.25s cubic-bezier(0,1,.5,1)}.tagify__dropdown__wrapper:hover{overflow:auto}.tagify__dropdown--initial .tagify__dropdown__wrapper{max-height:20px;transform:translateY(-1em)}.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper{transform:translateY(2em)}.tagify__dropdown__item{box-sizing:inherit;padding:.3em .5em;margin:1px;cursor:pointer;border-radius:2px;position:relative;outline:0}.tagify__dropdown__item--active{background:#3595f6;background:var(--tagify-dd-color-primary);color:#fff}.tagify__dropdown__item:active{filter:brightness(105%)} \ No newline at end of file diff --git a/server/user.jobengine.osgi.server/js/tagify.js b/server/user.jobengine.osgi.server/js/tagify.js new file mode 100644 index 00000000..04ab4780 --- /dev/null +++ b/server/user.jobengine.osgi.server/js/tagify.js @@ -0,0 +1,1961 @@ +/** + * Tagify (v 3.2.6)- tags input component + * By Yair Even-Or + * Don't sell this code. (c) + * https://github.com/yairEO/tagify + */ +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.Tagify = factory(); + } +}(this, function() { +"use strict"; + +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } + +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } + +function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } + +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +/** + * @constructor + * @param {Object} input DOM element + * @param {Object} settings settings object + */ +function Tagify(input, settings) { + // protection + if (!input) { + console.warn('Tagify: ', 'invalid input element ', input); + return this; + } + + this.applySettings(input, settings || {}); + this.state = { + editing: {}, + actions: {}, + // UI actions for state-locking + dropdown: {} + }; + this.value = []; // tags' data + // events' callbacks references will be stores here, so events could be unbinded + + this.listeners = {}; + this.DOM = {}; // Store all relevant DOM elements in an Object + + this.extend(this, new this.EventDispatcher(this)); + this.build(input); + this.getCSSVars(); + this.loadOriginalValues(); + this.events.customBinding.call(this); + this.events.binding.call(this); + input.autofocus && this.DOM.input.focus(); +} + +Tagify.prototype = { + isIE: window.document.documentMode, + // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility + TEXTS: { + empty: "empty", + exceed: "number of tags exceeded", + pattern: "pattern mismatch", + duplicate: "already exists", + notAllowed: "not allowed" + }, + DEFAULTS: { + delimiters: ",", + // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |." + pattern: null, + // RegEx pattern to validate input by. Ex: /[1-9]/ + maxTags: Infinity, + // Maximum number of tags + callbacks: {}, + // Exposed callbacks object to be triggered on certain events + addTagOnBlur: true, + // Flag - automatically adds the text which was inputed as a tag when blur event happens + duplicates: false, + // Flag - allow tuplicate tags + whitelist: [], + // Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting) + blacklist: [], + // A list of non-allowed tags + enforceWhitelist: false, + // Flag - Only allow tags allowed in whitelist + keepInvalidTags: false, + // Flag - if true, do not remove tags which did not pass validation + mixTagsAllowedAfter: /,|\.|\:|\s/, + // RegEx - Define conditions in which mix-tags content is allowing a tag to be added after + mixTagsInterpolator: ['[[', ']]'], + // Interpolation for mix mode. Everything between this will becmoe a tag + backspace: true, + // false / true / "edit" + skipInvalid: false, + // If `true`, do not add invalid, temporary, tags before automatically removing them + editTags: 2, + // 1 or 2 clicks to edit a tag. false/null for not allowing editing + transformTag: function transformTag() {}, + // Takes a tag input string as argument and returns a transformed value + autoComplete: { + enabled: true, + // Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text + rightKey: false // If `true`, when Right key is pressed, use the suggested value to create a tag, else just auto-completes the input. in mixed-mode this is set to "true" + + }, + dropdown: { + classname: '', + enabled: 2, + // minimum input characters needs to be typed for the dropdown to show + maxItems: 10, + searchKeys: [], + fuzzySearch: true, + highlightFirst: false, + // highlights first-matched item in the list + closeOnSelect: true, + // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown) + position: 'all' // 'manual' / 'text' / 'all' + + } + }, + // Using ARIA & role attributes + // https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html + templates: { + wrapper: function wrapper(input, settings) { + return ""); + }, + tag: function tag(value, tagData) { + return "\n \n
\n ").concat(value, "\n
\n
"); + }, + dropdownItem: function dropdownItem(item) { + var mapValueTo = this.settings.dropdown.mapValueTo, + value = (mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(item) : item[mapValueTo] : item.value) || item.value, + sanitizedValue = (value || item).replace(/`|'/g, "'"); + return "
").concat(sanitizedValue, "
"); + } + }, + customEventsList: ['add', 'remove', 'invalid', 'input', 'click', 'keydown', 'focus', 'blur', 'edit:input', 'edit:updated', 'edit:start', 'edit:keydown', 'dropdown:show', 'dropdown:hide', 'dropdown:select'], + applySettings: function applySettings(input, settings) { + var _this2 = this; + + this.DEFAULTS.templates = this.templates; + this.settings = this.extend({}, this.DEFAULTS, settings); + this.settings.readonly = input.hasAttribute('readonly'); // if "readonly" do not include an "input" element inside the Tags component + + this.settings.placeholder = input.getAttribute('placeholder') || this.settings.placeholder || ""; + if (this.isIE) this.settings.autoComplete = false; // IE goes crazy if this isn't false + + ["whitelist", "blacklist"].forEach(function (name) { + var attrVal = input.getAttribute('data-' + name); + + if (attrVal) { + attrVal = attrVal.split(_this2.settings.delimiters); + if (attrVal instanceof Array) _this2.settings[name] = attrVal; + } + }); // backward-compatibility for old version of "autoComplete" setting: + + if ("autoComplete" in settings && !this.isObject(settings.autoComplete)) { + this.settings.autoComplete = this.DEFAULTS.autoComplete; + this.settings.autoComplete.enabled = settings.autoComplete; + } + + if (input.pattern) try { + this.settings.pattern = new RegExp(input.pattern); + } catch (e) {} // Convert the "delimiters" setting into a REGEX object + + if (this.settings.delimiters) { + try { + this.settings.delimiters = new RegExp(this.settings.delimiters, "g"); + } catch (e) {} + } // make sure the dropdown will be shown on "focus" and not only after typing something (in "select" mode) + + + if (this.settings.mode == 'select') this.settings.dropdown.enabled = 0; + if (this.settings.mode == 'mix') this.settings.autoComplete.rightKey = true; + }, + + /** + * Creates a string of HTML element attributes + * @param {Object} data [Tag data] + */ + getAttributes: function getAttributes(data) { + // only items which are objects have properties which can be used as attributes + if (Object.prototype.toString.call(data) != "[object Object]") return ''; + var keys = Object.keys(data), + s = "", + propName, + i; + + for (i = keys.length; i--;) { + propName = keys[i]; + if (propName != 'class' && data.hasOwnProperty(propName) && data[propName]) s += " " + propName + (data[propName] ? "=\"".concat(data[propName], "\"") : ""); + } + + return s; + }, + + /** + * utility method + * https://stackoverflow.com/a/35385518/104380 + * @param {String} s [HTML string] + * @return {Object} [DOM node] + */ + parseHTML: function parseHTML(s) { + var parser = new DOMParser(), + node = parser.parseFromString(s.trim(), "text/html"); + return node.body.firstElementChild; + }, + + /** + * utility method + * https://stackoverflow.com/a/25396011/104380 + */ + escapeHTML: function escapeHTML(s) { + var text = document.createTextNode(s), + p = document.createElement('p'); + p.appendChild(text); + return p.innerHTML; + }, + + /** + * Get the caret position relative to the viewport + * https://stackoverflow.com/q/58985076/104380 + * + * @returns {object} left, top distance in pixels + */ + getCaretGlobalPosition: function getCaretGlobalPosition() { + var sel = document.getSelection(); + + if (sel.rangeCount) { + var r = sel.getRangeAt(0); + var node = r.startContainer; + var offset = r.startOffset; + var rect, r2; + + if (offset > 0) { + r2 = document.createRange(); + r2.setStart(node, offset - 1); + r2.setEnd(node, offset); + rect = r2.getBoundingClientRect(); + return { + left: rect.right, + top: rect.top, + bottom: rect.bottom + }; + } + } + + return { + left: -9999, + top: -9999 + }; + }, + + /** + * Get specific CSS variables which are relevant to this script and parse them as needed. + * The result is saved on the instance in "this.CSSVars" + */ + getCSSVars: function getCSSVars() { + var compStyle = getComputedStyle(this.DOM.scope, null); + + var getProp = function getProp(name) { + return compStyle.getPropertyValue('--' + name); + }; + + function seprateUnitFromValue(a) { + if (!a) return {}; + a = a.trim().split(' ')[0]; + var unit = a.split(/\d+/g).filter(function (n) { + return n; + }).pop().trim(), + value = +a.split(unit).filter(function (n) { + return n; + })[0].trim(); + return { + value: value, + unit: unit + }; + } + + this.CSSVars = { + tagHideTransition: function (_ref) { + var value = _ref.value, + unit = _ref.unit; + return unit == 's' ? value * 1000 : value; + }(seprateUnitFromValue(getProp('tag-hide-transition'))) + }; + }, + + /** + * builds the HTML of this component + * @param {Object} input [DOM element which would be "transformed" into "Tags"] + */ + build: function build(input) { + var DOM = this.DOM, + template = this.settings.templates.wrapper(input, this.settings); + DOM.originalInput = input; + DOM.scope = this.parseHTML(template); + DOM.input = DOM.scope.querySelector('[contenteditable]'); + input.parentNode.insertBefore(DOM.scope, input); + + if (this.settings.dropdown.enabled >= 0) { + this.dropdown.init.call(this); + } + }, + + /** + * revert any changes made by this component + */ + destroy: function destroy() { + this.DOM.scope.parentNode.removeChild(this.DOM.scope); + this.dropdown.hide.call(this, true); + }, + + /** + * if the original input had any values, add them as tags + */ + loadOriginalValues: function loadOriginalValues(value) { + value = value || this.DOM.originalInput.value; // if the original input already had any value (tags) + + if (!value) return; + this.removeAllTags(); + if (this.settings.mode == 'mix') this.parseMixTags(value.trim());else { + try { + if (typeof JSON.parse(value) !== 'string') value = JSON.parse(value); + } catch (err) {} + + this.addTags(value).forEach(function (tag) { + return tag && tag.classList.add('tagify--noAnim'); + }); + } + }, + + /** + * Checks if an argument is a javascript Object + */ + isObject: function isObject(obj) { + var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1); + return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement'; + }, + + /** + * merge objects into a single new one + * TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}}) + */ + extend: function extend(o, o1, o2) { + var that = this; + if (!(o instanceof Object)) o = {}; + copy(o, o1); + if (o2) copy(o, o2); + + function copy(a, b) { + // copy o2 to o + for (var key in b) { + if (b.hasOwnProperty(key)) { + if (that.isObject(b[key])) { + if (!that.isObject(a[key])) a[key] = Object.assign({}, b[key]);else copy(a[key], b[key]); + } else a[key] = b[key]; + } + } + } + + return o; + }, + cloneEvent: function cloneEvent(e) { + var clonedEvent = {}; + + for (var v in e) { + clonedEvent[v] = e[v]; + } + + return clonedEvent; + }, + + /** + * A constructor for exposing events to the outside + */ + EventDispatcher: function EventDispatcher(instance) { + // Create a DOM EventTarget object + var target = document.createTextNode(''); + + function addRemove(op, events, cb) { + if (cb) events.split(/\s+/g).forEach(function (name) { + return target[op + 'EventListener'].call(target, name, cb); + }); + } // Pass EventTarget interface calls to DOM EventTarget object + + + this.off = function (events, cb) { + addRemove('remove', events, cb); + return this; + }; + + this.on = function (events, cb) { + if (cb && typeof cb == 'function') addRemove('add', events, cb); + return this; + }; + + this.trigger = function (eventName, data) { + var e; + if (!eventName) return; + + if (instance.settings.isJQueryPlugin) { + if (eventName == 'remove') eventName = 'removeTag'; // issue #222 + + jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data]); + } else { + try { + e = new CustomEvent(eventName, { + "detail": this.extend({}, data, { + tagify: this + }) + }); + } catch (err) { + console.warn(err); + } + + target.dispatchEvent(e); + } + }; + }, + + /** + * Toogle loading state on/off + * @param {Boolean} isLoading + */ + loading: function loading(isLoading) { + // IE11 doesn't support toggle with second parameter + this.DOM.scope.classList[isLoading ? "add" : "remove"]('tagify--loading'); + return this; + }, + toggleFocusClass: function toggleFocusClass(force) { + this.DOM.scope.classList.toggle('tagify--focus', !!force); + }, + + /** + * DOM events listeners binding + */ + events: { + // bind custom events which were passed in the settings + customBinding: function customBinding() { + var _this3 = this; + + this.customEventsList.forEach(function (name) { + _this3.on(name, _this3.settings.callbacks[name]); + }); + }, + binding: function binding() { + var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; + + var _CB = this.events.callbacks, + _CBR, + action = bindUnbind ? 'addEventListener' : 'removeEventListener'; // do not allow the main events to be bound more than once + + + if (this.state.mainEvents && bindUnbind) return; // set the binding state of the main events, so they will not be bound more than once + + this.state.mainEvents = bindUnbind; + + if (bindUnbind && !this.listeners.main) { + // this event should never be unbinded: + // IE cannot register "input" events on contenteditable elements, so the "keydown" should be used instead.. + this.DOM.input.addEventListener(this.isIE ? "keydown" : "input", _CB[this.isIE ? "onInputIE" : "onInput"].bind(this)); + if (this.settings.isJQueryPlugin) jQuery(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this)); + } // setup callback references so events could be removed later + + + _CBR = this.listeners.main = this.listeners.main || { + focus: ['input', _CB.onFocusBlur.bind(this)], + blur: ['input', _CB.onFocusBlur.bind(this)], + keydown: ['input', _CB.onKeydown.bind(this)], + click: ['scope', _CB.onClickScope.bind(this)], + dblclick: ['scope', _CB.onDoubleClickScope.bind(this)] + }; + + for (var eventName in _CBR) { + // make sure the focus/blur event is always regesitered (and never more than once) + if (eventName == 'blur' && !bindUnbind) return; + + this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]); + } + }, + + /** + * DOM events callbacks + */ + callbacks: { + onFocusBlur: function onFocusBlur(e) { + var text = e.target ? e.target.textContent.trim() : '', + // a string + _s = this.settings, + type = e.type; // goes into this scenario only on input "blur" and a tag was clicked + + if (e.relatedTarget && e.relatedTarget.classList.contains('tagify__tag') && this.DOM.scope.contains(e.relatedTarget)) return; + + if (type == 'blur' && e.relatedTarget === this.DOM.scope) { + this.dropdown.hide.call(this); + this.DOM.input.focus(); + return; + } + + if (this.state.actions.selectOption && (_s.dropdown.enabled || !_s.dropdown.closeOnSelect)) return; + this.state.hasFocus = type == "focus" ? +new Date() : false; + this.toggleFocusClass(this.state.hasFocus); + this.setRangeAtStartEnd(false); + + if (_s.mode == 'mix') { + if (e.type == "blur") this.dropdown.hide.call(this); + return; + } + + if (type == "focus") { + this.trigger("focus", { + relatedTarget: e.relatedTarget + }); // e.target.classList.remove('placeholder'); + + if (_s.dropdown.enabled === 0 && _s.mode != "select") { + this.dropdown.show.call(this); + } + + return; + } else if (type == "blur") { + this.trigger("blur", { + relatedTarget: e.relatedTarget + }); + this.loading(false); // do not add a tag if "selectOption" action was just fired (this means a tag was just added from the dropdown) + + text && !this.state.actions.selectOption && _s.addTagOnBlur && this.addTags(text, true); + } + + this.DOM.input.removeAttribute('style'); + this.dropdown.hide.call(this); + }, + onKeydown: function onKeydown(e) { + var _this4 = this; + + var s = e.target.textContent.trim(), + tags; + this.trigger("keydown", { + originalEvent: this.cloneEvent(e) + }); + + if (this.settings.mode == 'mix') { + switch (e.key) { + case 'Left': + case 'ArrowLeft': + { + // when left arrow was pressed, raise a flag so when the dropdown is shown, right-arrow will be ignored + // because it seems likely the user wishes to use the arrows to move the caret + this.state.actions.ArrowLeft = true; + break; + } + + case 'Delete': + case 'Backspace': + { + var selection = document.getSelection(), + isFF = !!navigator.userAgent.match(/firefox/i); + if (isFF && selection && selection.anchorOffset == 0) this.removeTag(selection.anchorNode.previousSibling); + var values = []; // find out which tag(s) were deleted and update "this.value" accordingly + + tags = this.DOM.input.children; // a minimum delay is needed before the node actually gets ditached from the document (don't know why), + // to know exactly which tag was deleted. This is the easiest way of knowing besides using MutationObserver + + setTimeout(function () { + // iterate over the list of tags still in the document and then filter only those from the "this.value" collection + [].forEach.call(tags, function (tagElm) { + return values.push(tagElm.getAttribute('value')); + }); + _this4.value = _this4.value.filter(function (d) { + return values.indexOf(d.value) != -1; + }); + }); + break; + } + // currently commented to allow new lines in mixed-mode + // case 'Enter' : + // e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 + } + + return true; + } + + switch (e.key) { + case 'Backspace': + if (s == "" || s.charCodeAt(0) == 8203) { + // 8203: ZERO WIDTH SPACE unicode + if (this.settings.backspace === true) this.removeTag();else if (this.settings.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); // timeout reason: when edited tag gets focused and the caret is placed at the end, the last character gets deletec (because of backspace) + } + + break; + + case 'Esc': + case 'Escape': + if (this.state.dropdown.visible) return; + e.target.blur(); + break; + + case 'Down': + case 'ArrowDown': + // if( this.settings.mode == 'select' ) // issue #333 + if (!this.state.dropdown.visible) this.dropdown.show.call(this); + break; + + case 'ArrowRight': + { + var tagData = this.state.inputSuggestion || this.state.ddItemData; + + if (tagData && this.settings.autoComplete.rightKey) { + this.addTags([tagData], true); + return; + } + + break; + } + + case 'Tab': + { + if (!s) return true; + } + + case 'Enter': + e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380 + // because the main "keydown" event is bound before the dropdown events, this will fire first and will not *yet* + // know if an option was just selected from the dropdown menu. If an option was selected, + // the dropdown events should handle adding the tag + + setTimeout(function () { + if (_this4.state.actions.selectOption) return; + + _this4.addTags(s, true); + }); + } + }, + onInput: function onInput(e) { + var value = this.settings.mode == 'mix' ? this.DOM.input.textContent : this.input.normalize.call(this), + showSuggestions = value.length >= this.settings.dropdown.enabled, + data = { + value: value, + inputElm: this.DOM.input + }; + if (this.settings.mode == 'mix') return this.events.callbacks.onMixTagsInput.call(this, e); + + if (!value) { + this.input.set.call(this, ''); + return; + } + + if (this.input.value == value) return; // for IE; since IE doesn't have an "input" event so "keyDown" is used instead + + data.isValid = this.validateTag(value); + this.trigger('input', data); // "input" event must be triggered at this point, before the dropdown is shown + // save the value on the input's State object + + this.input.set.call(this, value, false); // update the input with the normalized value and run validations + // this.setRangeAtStartEnd(); // fix caret position + + if (value.search(this.settings.delimiters) != -1) { + if (this.addTags(value)) { + this.input.set.call(this); // clear the input field's value + } + } else if (this.settings.dropdown.enabled >= 0) { + this.dropdown[showSuggestions ? "show" : "hide"].call(this, value); + } + }, + onMixTagsInput: function onMixTagsInput(e) { + var _this5 = this; + + var sel, + range, + split, + tag, + showSuggestions, + _s = this.settings; + if (this.hasMaxTags()) return true; + + if (window.getSelection) { + sel = window.getSelection(); + + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0).cloneRange(); + range.collapse(true); + range.setStart(window.getSelection().focusNode, 0); + split = range.toString().split(_s.mixTagsAllowedAfter); // ["foo", "bar", "@a"] + + tag = split[split.length - 1].match(_s.pattern); + + if (tag) { + this.state.actions.ArrowLeft = false; // start fresh, assuming the user did not (yet) used any arrow to move the caret + + this.state.tag = { + prefix: tag[0], + value: tag.input.split(tag[0])[1] + }; + showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled; + } + } + } + + this.update(); // wait until the "this.value" has been updated (see "onKeydown" method for "mix-mode") + // the dropdown must be shown only after this event has been driggered, so an implementer could + // dynamically change the whitelist. + + setTimeout(function () { + _this5.trigger("input", _this5.extend({}, _this5.state.tag, { + textContent: _this5.DOM.input.textContent + })); + + if (_this5.state.tag) _this5.dropdown[showSuggestions ? "show" : "hide"].call(_this5, _this5.state.tag.value); + }, 10); + }, + onInputIE: function onInputIE(e) { + var _this = this; // for the "e.target.textContent" to be changed, the browser requires a small delay + + + setTimeout(function () { + _this.events.callbacks.onInput.call(_this, e); + }); + }, + onClickScope: function onClickScope(e) { + var tagElm = e.target.closest('.tagify__tag'), + _s = this.settings, + timeDiffFocus = +new Date() - this.state.hasFocus, + tagElmIdx; + + if (e.target == this.DOM.scope) { + // if( !this.state.hasFocus ) + // this.dropdown.hide.call(this) + this.DOM.input.focus(); + return; + } else if (e.target.classList.contains("tagify__tag__removeBtn")) { + this.removeTag(e.target.parentNode); + return; + } else if (tagElm) { + tagElmIdx = this.getNodeIndex(tagElm); + this.trigger("click", { + tag: tagElm, + index: tagElmIdx, + data: this.value[tagElmIdx], + originalEvent: this.cloneEvent(e) + }); + if (this.settings.editTags == 1) this.events.callbacks.onDoubleClickScope.call(this, e); + return; + } // when clicking on the input itself + else if (e.target == this.DOM.input && timeDiffFocus > 500) { + if (this.state.dropdown.visible) this.dropdown.hide.call(this);else if (_s.dropdown.enabled === 0 && _s.mode != 'mix') this.dropdown.show.call(this); + return; + } + + if (_s.mode == 'select') !this.state.dropdown.visible && this.dropdown.show.call(this); + }, + onEditTagInput: function onEditTagInput(editableElm, e) { + var tagElm = editableElm.closest('tag'), + tagElmIdx = this.getNodeIndex(tagElm), + value = this.input.normalize.call(this, editableElm), + isValid = value.toLowerCase() == editableElm.originalValue.toLowerCase() || this.validateTag(value); + tagElm.classList.toggle('tagify--invalid', isValid !== true); + tagElm.isValid = isValid; // show dropdown if typed text is equal or more than the "enabled" dropdown setting + + if (value.length >= this.settings.dropdown.enabled) { + this.state.editing.value = value; + this.dropdown.show.call(this, value); + } + + this.trigger("edit:input", { + tag: tagElm, + index: tagElmIdx, + data: this.extend({}, this.value[tagElmIdx], { + newValue: value + }), + originalEvent: this.cloneEvent(e) + }); + }, + onEditTagBlur: function onEditTagBlur(editableElm) { + if (!this.state.hasFocus) this.toggleFocusClass(); + if (!this.DOM.scope.contains(editableElm)) return; + + var tagElm = editableElm.closest('.tagify__tag'), + tagElmIdx = this.getNodeIndex(tagElm), + currentValue = this.input.normalize.call(this, editableElm), + value = currentValue || editableElm.originalValue, + hasChanged = value != editableElm.originalValue, + isValid = tagElm.isValid, + tagData = _objectSpread({}, this.value[tagElmIdx], { + value: value + }); // this.DOM.input.focus() + + + if (!currentValue) { + this.removeTag(tagElm); + return; + } + + if (hasChanged) { + this.settings.transformTag.call(this, tagData); // re-validate after tag transformation + + isValid = this.validateTag(tagData.value); + } else { + this.onEditTagDone(tagElm); + return; + } + + if (isValid !== undefined && isValid !== true) return; + this.onEditTagDone(tagElm, tagData); + }, + onEditTagkeydown: function onEditTagkeydown(e) { + this.trigger("edit:keydown", { + originalEvent: this.cloneEvent(e) + }); + + switch (e.key) { + case 'Esc': + case 'Escape': + e.target.textContent = e.target.originalValue; + + case 'Enter': + case 'Tab': + e.preventDefault(); + e.target.blur(); + } + }, + onDoubleClickScope: function onDoubleClickScope(e) { + var tagElm = e.target.closest('tag'), + _s = this.settings, + isEditingTag, + isReadyOnlyTag; + if (!tagElm) return; + isEditingTag = tagElm.classList.contains('tagify__tag--editable'), isReadyOnlyTag = tagElm.hasAttribute('readonly'); + if (_s.mode != 'select' && !_s.readonly && !isEditingTag && !isReadyOnlyTag && this.settings.editTags) this.editTag(tagElm); + this.toggleFocusClass(true); + } + } + }, + + /** + * @param {Node} tagElm the tag element to edit. if nothing specified, use last last + */ + editTag: function editTag() { + var _this6 = this; + + var tagElm = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getLastTag(); + + var editableElm = tagElm.querySelector('.tagify__tag-text'), + tagIdx = this.getNodeIndex(tagElm), + tagData = this.value[tagIdx], + _CB = this.events.callbacks, + that = this, + delayed_onEditTagBlur = function delayed_onEditTagBlur() { + setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm); + }; + + if (!editableElm) { + console.warn('Cannot find element in Tag template: ', '.tagify__tag-text'); + return; + } + + if ("editable" in tagData && !tagData.editable) return; + tagElm.classList.add('tagify__tag--editable'); + editableElm.originalValue = editableElm.textContent; + editableElm.setAttribute('contenteditable', true); + editableElm.addEventListener('blur', delayed_onEditTagBlur); + editableElm.addEventListener('input', _CB.onEditTagInput.bind(this, editableElm)); + editableElm.addEventListener('keydown', function (e) { + return _CB.onEditTagkeydown.call(_this6, e); + }); + editableElm.focus(); + this.setRangeAtStartEnd(false, editableElm); + this.state.editing = { + scope: tagElm, + input: tagElm.querySelector("[contenteditable]") + }; + this.trigger("edit:start", { + tag: tagElm, + index: tagIdx, + data: tagData + }); + return this; + }, + onEditTagDone: function onEditTagDone(tagElm, tagData) { + var eventData = { + tag: tagElm, + index: this.getNodeIndex(tagElm), + data: tagData + }; + this.trigger("edit:beforeUpdate", eventData); + this.replaceTag(tagElm, tagData); + this.trigger("edit:updated", eventData); + }, + + /** + * Exit a tag's edit-mode. + * if "tagData" exists, replace the tag element with new data and update Tagify value + */ + replaceTag: function replaceTag(tagElm, tagData) { + var _this7 = this; + + var editableElm = tagElm.querySelector('.tagify__tag-text'), + clone = editableElm.cloneNode(true), + tagElmIdx = this.getNodeIndex(tagElm); + if (this.state.editing.locked) return; // when editing a tag and selecting a dropdown suggested item, the state should be "locked" + // so "onEditTagBlur" won't run and change the tag also *after* it was just changed. + + this.state.editing = { + locked: true + }; + setTimeout(function () { + return delete _this7.state.editing.locked; + }, 500); // update DOM nodes + + clone.removeAttribute('contenteditable'); + tagElm.classList.remove('tagify__tag--editable'); // guarantee to remove all events which were added by the "editTag" method + + editableElm.parentNode.replaceChild(clone, editableElm); // continue only if there was a reason for it + + if (tagData) { + clone.innerHTML = tagData.value; + clone.title = tagData.value; // update data + + this.value[tagElmIdx] = tagData; + this.update(); + } + }, + + /** https://stackoverflow.com/a/59156872/104380 + * @param {Boolean} start indicating where to place it (start or end of the node) + * @param {Object} node DOM node to place the caret at + */ + setRangeAtStartEnd: function setRangeAtStartEnd(start, node) { + node = node || this.DOM.input; + node = node.lastChild || node; + var sel = document.getSelection(); + + if (sel.rangeCount) { + ['Start', 'End'].forEach(function (pos) { + return sel.getRangeAt(0)["set" + pos](node, start ? 0 : node.length); + }); + } + }, + + /** + * input bridge for accessing & setting + * @type {Object} + */ + input: { + value: '', + set: function set() { + var s = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var updateDOM = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + var hideDropdown = this.settings.dropdown.closeOnSelect; + this.input.value = s; + if (updateDOM) this.DOM.input.innerHTML = s; + if (!s && hideDropdown) setTimeout(this.dropdown.hide.bind(this), 20); // setTimeout duration must be HIGER than the dropdown's item "onClick" method's "focus()" event, because the "hide" method re-binds the main events and it will catch the "blur" event and will cause + + this.input.autocomplete.suggest.call(this); + this.input.validate.call(this); + }, + + /** + * Marks the tagify's input as "invalid" if the value did not pass "validateTag()" + */ + validate: function validate() { + var isValid = !this.input.value || this.validateTag(this.input.value); + if (this.settings.mode == 'select') this.DOM.scope.classList.toggle('tagify--invalid', isValid !== true);else this.DOM.input.classList.toggle('tagify__input--invalid', isValid !== true); + }, + // remove any child DOM elements that aren't of type TEXT (like
) + normalize: function normalize(node) { + var clone = node || this.DOM.input, + //.cloneNode(true), + v = []; // when a text was pasted in FF, the "this.DOM.input" element will have
but no newline symbols (\n), and this will + // result in tags no being properly created if one wishes to create a separate tag per newline. + + clone.childNodes.forEach(function (n) { + return n.nodeType == 3 && v.push(n.nodeValue); + }); + v = v.join("\n"); + + try { + // "delimiters" might be of a non-regex value, where this will fail ("Tags With Properties" example in demo page): + v = v.replace(/(?:\r\n|\r|\n)/g, this.settings.delimiters.source.charAt(0)); + } catch (err) {} + + v = v.replace(/\s/g, ' ') // replace NBSPs with spaces characters + .replace(/^\s+/, ""); // trimLeft + + return v; + }, + + /** + * suggest the rest of the input's value (via CSS "::after" using "content:attr(...)") + * @param {String} s [description] + */ + autocomplete: { + suggest: function suggest(data) { + if (!this.settings.autoComplete.enabled) return; + data = data || {}; + if (typeof data == 'string') data = { + value: data + }; + var suggestedText = data.value || '', + suggestionStart = suggestedText.substr(0, this.input.value.length).toLowerCase(), + suggestionTrimmed = suggestedText.substring(this.input.value.length); + + if (!suggestedText || !this.input.value || suggestionStart != this.input.value.toLowerCase()) { + this.DOM.input.removeAttribute("data-suggest"); + delete this.state.inputSuggestion; + } else { + this.DOM.input.setAttribute("data-suggest", suggestionTrimmed); + this.state.inputSuggestion = data; + } + }, + + /** + * sets the suggested text as the input's value & cleanup the suggestion autocomplete. + * @param {String} s [text] + */ + set: function set(s) { + var dataSuggest = this.DOM.input.getAttribute('data-suggest'), + suggestion = s || (dataSuggest ? this.input.value + dataSuggest : null); + + if (suggestion) { + if (this.settings.mode == 'mix') { + this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix + suggestion)); + } else { + this.input.set.call(this, suggestion); + this.setRangeAtStartEnd(); + } + + this.input.autocomplete.suggest.call(this); + this.dropdown.hide.call(this); + return true; + } + + return false; + } + } + }, + getNodeIndex: function getNodeIndex(node) { + var index = 0; + if (node) while (node = node.previousElementSibling) { + index++; + } + return index; + }, + getTagElms: function getTagElms() { + return this.DOM.scope.querySelectorAll('.tagify__tag'); + }, + getLastTag: function getLastTag() { + var lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide):not([readonly])'); + return lastTag[lastTag.length - 1]; + }, + + /** + * Searches if any tag with a certain value already exis + * @param {String/Object} v [text value / tag data object] + * @return {Boolean} + */ + isTagDuplicate: function isTagDuplicate(v) { + var _this8 = this; + + // duplications are irrelevant for this scenario + if (this.settings.mode == 'select') return false; + return this.value.some(function (item) { + return _this8.isObject(v) ? JSON.stringify(item).toLowerCase() === JSON.stringify(v).toLowerCase() : v.trim().toLowerCase() === item.value.toLowerCase(); + }); + }, + getTagIndexByValue: function getTagIndexByValue(value) { + var result = []; + this.getTagElms().forEach(function (tagElm, i) { + if (tagElm.textContent.trim().toLowerCase() == value.toLowerCase()) result.push(i); + }); + return result; + }, + getTagElmByValue: function getTagElmByValue(value) { + var tagIdx = this.getTagIndexByValue(value)[0]; + return this.getTagElms()[tagIdx]; + }, + + /** + * Mark a tag element by its value + * @param {String|Number} value [text value to search for] + * @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings] + * @return {boolean} [found / not found] + */ + markTagByValue: function markTagByValue(value, tagElm) { + tagElm = tagElm || this.getTagElmByValue(value); // check AGAIN if "tagElm" is defined + + if (tagElm) { + tagElm.classList.add('tagify--mark'); // setTimeout(() => { tagElm.classList.remove('tagify--mark') }, 100); + + return tagElm; + } + + return false; + }, + + /** + * make sure the tag, or words in it, is not in the blacklist + */ + isTagBlacklisted: function isTagBlacklisted(v) { + v = v.toLowerCase().trim(); + return this.settings.blacklist.filter(function (x) { + return v == x.toLowerCase(); + }).length; + }, + + /** + * make sure the tag, or words in it, is not in the blacklist + */ + isTagWhitelisted: function isTagWhitelisted(v) { + return this.settings.whitelist.some(function (item) { + return typeof v == 'string' ? v.trim().toLowerCase() === (item.value || item).toLowerCase() : JSON.stringify(item).toLowerCase() === JSON.stringify(v).toLowerCase(); + }); + }, + + /** + * validate a tag object BEFORE the actual tag will be created & appeneded + * @param {String} s + * @return {Boolean/String} ["true" if validation has passed, String for a fail] + */ + validateTag: function validateTag(s) { + var value = s.trim(), + _s = this.settings, + result = true; // check for empty value + + if (!value) result = this.TEXTS.empty; // check if pattern should be used and if so, use it to test the value + else if (_s.pattern && !_s.pattern.test(value)) result = this.TEXTS.pattern; // if duplicates are not allowed and there is a duplicate + else if (!_s.duplicates && this.isTagDuplicate(value)) result = this.TEXTS.duplicate;else if (this.isTagBlacklisted(value) || _s.enforceWhitelist && !this.isTagWhitelisted(value)) result = this.TEXTS.notAllowed; + return result; + }, + hasMaxTags: function hasMaxTags() { + if (this.value.length >= this.settings.maxTags) return this.TEXTS.exceed; + return false; + }, + + /** + * pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words + * so each item should be iterated on and a tag created for. + * @return {Array} [Array of Objects] + */ + normalizeTags: function normalizeTags(tagsItems) { + var _this$settings = this.settings, + whitelist = _this$settings.whitelist, + delimiters = _this$settings.delimiters, + mode = _this$settings.mode, + whitelistWithProps = whitelist ? whitelist[0] instanceof Object : false, + isArray = tagsItems instanceof Array, + isCollection = isArray && tagsItems[0] instanceof Object && "value" in tagsItems[0], + temp = [], + mapStringToCollection = function mapStringToCollection(s) { + return s.split(delimiters).filter(function (n) { + return n; + }).map(function (v) { + return { + value: v.trim() + }; + }); + }; // no need to continue if "tagsItems" is an Array of Objects + + + if (isCollection) { + var _ref2; + + // iterate the collection items and check for values that can be splitted into multiple tags + tagsItems = (_ref2 = []).concat.apply(_ref2, _toConsumableArray(tagsItems.map(function (item) { + return mapStringToCollection(item.value).map(function (newItem) { + return _objectSpread({}, item, {}, newItem); + }); + }))); + return tagsItems; + } + + if (typeof tagsItems == 'number') tagsItems = tagsItems.toString(); // if the value is a "simple" String, ex: "aaa, bbb, ccc" + + if (typeof tagsItems == 'string') { + if (!tagsItems.trim()) return []; // go over each tag and add it (if there were multiple ones) + + tagsItems = mapStringToCollection(tagsItems); + } else if (isArray) { + var _ref3; + + tagsItems = (_ref3 = []).concat.apply(_ref3, _toConsumableArray(tagsItems.map(function (item) { + return mapStringToCollection(item); + }))); + } // search if the tag exists in the whitelist as an Object (has props), + // to be able to use its properties + + + if (whitelistWithProps) { + tagsItems.forEach(function (item) { + // the "value" prop should preferably be unique + var matchObj = whitelist.filter(function (WL_item) { + return WL_item.value.toLowerCase() == item.value.toLowerCase(); + }); + + if (matchObj[0]) { + temp.push(matchObj[0]); // set the Array (with the found Object) as the new value + } else if (mode != 'mix') temp.push(item); + }); + tagsItems = temp; + } + + return tagsItems; + }, + + /** + * Used to parse the initial value of a textarea (or input) element and gemerate mixed text w/ tags + * https://stackoverflow.com/a/57598892/104380 + * @param {String} s + */ + parseMixTags: function parseMixTags(s) { + var _this9 = this; + + var _this$settings2 = this.settings, + mixTagsInterpolator = _this$settings2.mixTagsInterpolator, + duplicates = _this$settings2.duplicates, + transformTag = _this$settings2.transformTag, + enforceWhitelist = _this$settings2.enforceWhitelist; + s = s.split(mixTagsInterpolator[0]).map(function (s1, i) { + var s2 = s1.split(mixTagsInterpolator[1]), + preInterpolated = s2[0], + tagData, + tagElm; + + try { + tagData = JSON.parse(preInterpolated); + } catch (err) { + tagData = _this9.normalizeTags(preInterpolated)[0]; //{value:preInterpolated} + } + + if (s2.length > 1 && (!enforceWhitelist || _this9.isTagWhitelisted(tagData.value)) && !(!duplicates && _this9.isTagDuplicate(tagData))) { + transformTag.call(_this9, tagData); + tagElm = _this9.createTagElem(tagData); + s2[0] = tagElm.outerHTML; //+ "⁠" // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag) + + _this9.value.push(tagData); + } else if (s1) return i ? mixTagsInterpolator[0] + s1 : s1; + + return s2.join(''); + }).join(''); + this.DOM.input.innerHTML = s; + this.DOM.input.appendChild(document.createTextNode('')); + this.update(); + return s; + }, + + /** + * For mixed-mode: replaces a text starting with a prefix with a wrapper element (tag or something) + * First there *has* to be a "this.state.tag" which is a string that was just typed and is staring with a prefix + */ + replaceTextWithNode: function replaceTextWithNode(wrapperElm, tagString) { + if (!this.state.tag && !tagString) return; + tagString = tagString || this.state.tag.prefix + this.state.tag.value; + var idx, + replacedNode, + selection = window.getSelection(), + nodeAtCaret = selection.anchorNode; // ex. replace #ba with the tag "bart" where "|" is where the caret is: + // start with: "#ba #ba| #ba" + // split the text node at the index of the caret + + nodeAtCaret.splitText(selection.anchorOffset); // "#ba #ba" + // get index of last occurence of "#ba" + + idx = nodeAtCaret.nodeValue.lastIndexOf(tagString); + replacedNode = nodeAtCaret.splitText(idx); // clean up the tag's string and put tag element instead + + replacedNode.nodeValue = replacedNode.nodeValue.replace(tagString, ''); + nodeAtCaret.parentNode.insertBefore(wrapperElm, replacedNode); + this.DOM.input.normalize(); + return replacedNode; + }, + + /** + * For selecting a single option (not used for multiple tags) + * @param {Object} tagElm Tag DOM node + * @param {Object} tagData Tag data + */ + selectTag: function selectTag(tagElm, tagData) { + this.input.set.call(this, tagData.value, true); + setTimeout(this.setRangeAtStartEnd.bind(this)); + if (this.getLastTag()) this.replaceTag(this.getLastTag(), tagData);else this.appendTag(tagElm); + this.value[0] = tagData; + this.trigger('add', { + tag: tagElm, + data: tagData + }); + this.update(); + return [tagElm]; + }, + + /** + * add an empty "tag" element in an editable state + */ + addEmptyTag: function addEmptyTag() { + var tagData = { + value: "" + }, + tagElm = this.createTagElem(tagData); // add the tag to the component's DOM + + this.appendTag(tagElm); + this.value.push(tagData); + this.update(); + this.editTag(tagElm); + }, + + /** + * add a "tag" element to the "tags" component + * @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] + * @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags] + * @param {Boolean} skipInvalid [do not add, mark & remove invalid tags] + * @return {Array} Array of DOM elements (tags) + */ + addTags: function addTags(tagsItems, clearInput) { + var _this10 = this; + + var skipInvalid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.settings.skipInvalid; + var tagElems = [], + tagElm, + _s = this.settings; + + if (!tagsItems || tagsItems.length == 0) { + // is mode is "select" clean all tags + if (_s.mode == 'select') this.removeAllTags(); + return tagElems; + } // converts Array/String/Object to an Array of Objects + + + tagsItems = this.normalizeTags(tagsItems); // if in edit-mode, do not continue but instead replace the tag's text + + if (this.state.editing.scope) { + return this.onEditTagDone(this.state.editing.scope, tagsItems[0]); + } + + if (_s.mode == 'mix') { + _s.transformTag.call(this, tagsItems[0]); + + tagElm = this.createTagElem(tagsItems[0]); // insert the new tag to the END if "addTags" was called from outside + + if (!this.replaceTextWithNode(tagElm)) { + this.DOM.input.appendChild(tagElm); + } // fixes a firefox bug where if the last child of the input is a tag and not a text, the input cannot get focus (by Tab key) + + + this.DOM.input.appendChild(document.createTextNode('')); + tagsItems[0].prefix = tagsItems[0].prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source || _s.pattern)[0]; + this.value.push(tagsItems[0]); + this.update(); + this.state.tag = null; + this.trigger('add', this.extend({}, { + tag: tagElm + }, { + data: tagsItems[0] + })); // fixes a firefox bug where if the last child of the input is a tag and not a text, the input cannot get focus (by Tab key) + + this.DOM.input.appendChild(document.createTextNode('')); + return tagElm; + } + + if (_s.mode == 'select') clearInput = false; + this.DOM.input.removeAttribute('style'); + tagsItems.forEach(function (tagData) { + var tagValidation, + tagElm, + tagElmParams = {}; // shallow-clone tagData so later modifications will not apply to the source + + tagData = Object.assign({}, tagData); + + _s.transformTag.call(_this10, tagData); ///////////////// ( validation )////////////////////// + + + tagValidation = _this10.hasMaxTags() || _this10.validateTag(tagData.value); + + if (tagValidation !== true) { + if (skipInvalid) return; + tagElmParams["aria-invalid"] = true; + tagElmParams["class"] = (tagData["class"] || '') + ' tagify--notAllowed'; + tagElmParams.title = tagValidation; + + _this10.markTagByValue(tagData.value); + } ///////////////////////////////////////////////////// + // add accessibility attributes + + + tagElmParams.role = "tag"; + if (tagData.readonly) tagElmParams["aria-readonly"] = true; // Create tag HTML element + + tagElm = _this10.createTagElem(_this10.extend({}, tagData, tagElmParams)); + tagElems.push(tagElm); // mode-select overrides + + if (_s.mode == 'select') { + return _this10.selectTag(tagElm, tagData); + } // add the tag to the component's DOM + + + _this10.appendTag(tagElm); + + if (tagValidation === true) { + // update state + _this10.value.push(tagData); + + _this10.update(); + + _this10.trigger('add', { + tag: tagElm, + index: _this10.value.length - 1, + data: tagData + }); + } else { + _this10.trigger("invalid", { + data: tagData, + index: _this10.value.length, + tag: tagElm, + message: tagValidation + }); + + if (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false") + setTimeout(function () { + return _this10.removeTag(tagElm, true); + }, 1000); + } + + _this10.dropdown.position.call(_this10); // reposition the dropdown because the just-added tag might cause a new-line + + }); + + if (tagsItems.length && clearInput) { + this.input.set.call(this); + } + + this.dropdown.refilter.call(this); + return tagElems; + }, + + /** + * appened (validated) tag to the component's DOM scope + */ + appendTag: function appendTag(tagElm) { + var insertBeforeNode = this.DOM.scope.lastElementChild; + if (insertBeforeNode === this.DOM.input) this.DOM.scope.insertBefore(tagElm, insertBeforeNode);else this.DOM.scope.appendChild(tagElm); + }, + + /** + * Removed new lines and irrelevant spaces which might affect layout, and are better gone + * @param {string} s [HTML string] + */ + minify: function minify(s) { + return s ? s.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g, function (m, $1) { + return $1 ? $1 : ' '; + }) // https://stackoverflow.com/a/44841484/104380 + : ""; + }, + + /** + * creates a DOM tag element and injects it into the component (this.DOM.scope) + * @param {Object} tagData [text value & properties for the created tag] + * @return {Object} [DOM element] + */ + createTagElem: function createTagElem(tagData) { + var tagElm, + v = this.escapeHTML(tagData.value), + template = this.settings.templates.tag.call(this, v, tagData); + if (this.settings.readonly) tagData.readonly = true; + template = this.minify(template); + tagElm = this.parseHTML(template); + return tagElm; + }, + + /** + * Removes a tag + * @param {Object|String} tagElm [DOM element or a String value. if undefined or null, remove last added tag] + * @param {Boolean} silent [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify] + * @param {Number} tranDuration [Transition duration in MS] + */ + removeTag: function removeTag(tagElm, silent, tranDuration) { + tagElm = tagElm || this.getLastTag(); + tranDuration = tranDuration || this.CSSVars.tagHideTransition; + if (typeof tagElm == 'string') tagElm = this.getTagElmByValue(tagElm); + if (!(tagElm instanceof HTMLElement)) return; + var tagData, + that = this, + tagIdx = this.getNodeIndex(tagElm); // this.getTagIndexByValue(tagElm.textContent) + + if (this.settings.mode == 'select') { + tranDuration = 0; + this.input.set.call(this); + } + + if (tagElm.classList.contains('tagify--notAllowed')) silent = true; + + function removeNode() { + if (!tagElm.parentNode) return; + tagElm.parentNode.removeChild(tagElm); + + if (!silent) { + tagData = that.value.splice(tagIdx, 1)[0]; // remove the tag from the data object + + that.update(); // update the original input with the current value + + that.trigger('remove', { + tag: tagElm, + index: tagIdx, + data: tagData + }); + that.dropdown.refilter.call(that); + that.dropdown.position.call(that); + } else if (that.settings.keepInvalidTags) that.trigger('remove', { + tag: tagElm, + index: tagIdx + }); + } + + function animation() { + 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(removeNode, tranDuration); + } + + if (tranDuration && tranDuration > 10) animation();else removeNode(); + }, + removeAllTags: function removeAllTags() { + this.value = []; + this.update(); + Array.prototype.slice.call(this.getTagElms()).forEach(function (elm) { + return elm.parentNode.removeChild(elm); + }); + this.dropdown.position.call(this); + if (this.settings.mode == 'select') this.input.set.call(this); + }, + preUpdate: function preUpdate() { + this.DOM.scope.classList.toggle('tagify--hasMaxTags', this.value.length >= this.settings.maxTags); + this.DOM.scope.classList.toggle('tagify--noTags', !this.value.length); + }, + + /** + * update the origianl (hidden) input field's value + * see - https://stackoverflow.com/q/50957841/104380 + */ + update: function update() { + this.preUpdate(); + this.DOM.originalInput.value = this.settings.mode == 'mix' ? this.getMixedTagsAsString() : this.value.length ? JSON.stringify(this.value) : ""; + }, + getMixedTagsAsString: function getMixedTagsAsString() { + var _this11 = this; + + var result = "", + i = 0, + _interpolator = this.settings.mixTagsInterpolator; + this.DOM.input.childNodes.forEach(function (node) { + if (node.nodeType == 1 && node.classList.contains("tagify__tag")) result += _interpolator[0] + JSON.stringify(_this11.value[i++]) + _interpolator[1];else result += node.textContent; + }); + return result; + }, + + /** + * Meassures an element's height, which might yet have been added DOM + * https://stackoverflow.com/q/5944038/104380 + * @param {DOM} node + */ + getNodeHeight: function getNodeHeight(node) { + var height, + clone = node.cloneNode(true); + clone.style.cssText = "position:fixed; top:-9999px; opacity:0"; + document.body.appendChild(clone); + height = clone.clientHeight; + clone.parentNode.removeChild(clone); + return height; + }, + + /** + * Dropdown controller + * @type {Object} + */ + dropdown: { + init: function init() { + this.DOM.dropdown = this.dropdown.build.call(this); + this.DOM.dropdown.content = this.DOM.dropdown.querySelector('.tagify__dropdown__wrapper'); + }, + build: function build() { + var _this$settings$dropdo = this.settings.dropdown, + position = _this$settings$dropdo.position, + classname = _this$settings$dropdo.classname, + _className = "".concat(position == 'manual' ? "" : "tagify__dropdown tagify__dropdown--".concat(position), " ").concat(classname).trim(), + elm = this.parseHTML("
\n
\n
")); + + return elm; + }, + show: function show(value) { + var _this12 = this; + + var listHTML, + _s = this.settings, + firstListItem, + firstListItemValue, + ddHeight, + isManual = _s.dropdown.position == 'manual'; + if (!_s.whitelist || !_s.whitelist.length || _s.dropdown.enable === false) return; // if no value was supplied, show all the "whitelist" items in the dropdown + // @type [Array] listItems + // TODO: add a Setting to control items' sort order for "listItems" + + this.suggestedListItems = this.dropdown.filterListItems.call(this, value); // hide suggestions list if no suggestions were matched + + if (this.suggestedListItems.length) { + firstListItem = this.suggestedListItems[0]; + firstListItemValue = firstListItem.value || firstListItem; + + if (_s.autoComplete) { + // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of "fuzzysearch" setting) + if (firstListItemValue.indexOf(value) == 0) this.input.autocomplete.suggest.call(this, firstListItem); + } + } else { + this.input.autocomplete.suggest.call(this); + this.dropdown.hide.call(this); + return; + } + + listHTML = this.dropdown.createListHTML.call(this, this.suggestedListItems); + this.DOM.dropdown.content.innerHTML = this.minify(listHTML); // if "enforceWhitelist" is "true", highlight the first suggested item + + if (_s.enforceWhitelist && !isManual || _s.dropdown.highlightFirst) this.dropdown.highlightOption.call(this, this.DOM.dropdown.content.children[0]); + this.DOM.scope.setAttribute("aria-expanded", true); + this.trigger("dropdown:show", this.DOM.dropdown); // set the dropdown visible state to be the same as the searched value. + // MUST be set *before* position() is called + + this.state.dropdown.visible = value || true; + this.dropdown.position.call(this); // if the dropdown has yet to be appended to the document, + // append the dropdown to the body element & handle events + + if (!document.body.contains(this.DOM.dropdown)) { + if (!isManual) { + this.events.binding.call(this, false); // unbind the main events + // let the element render in the DOM first to accurately measure it + // this.DOM.dropdown.style.cssText = "left:-9999px; top:-9999px;"; + + ddHeight = this.getNodeHeight(this.DOM.dropdown); + this.DOM.dropdown.classList.add('tagify__dropdown--initial'); + this.dropdown.position.call(this, ddHeight); + document.body.appendChild(this.DOM.dropdown); + setTimeout(function () { + return _this12.DOM.dropdown.classList.remove('tagify__dropdown--initial'); + }); + } // timeout is needed for when pressing arrow down to show the dropdown, + // so the key event won't get registered in the dropdown events listeners + + + setTimeout(this.dropdown.events.binding.bind(this)); + } + }, + hide: function hide(force) { + var _this$DOM = this.DOM, + scope = _this$DOM.scope, + dropdown = _this$DOM.dropdown, + isManual = this.settings.dropdown.position == 'manual' && !force; + if (!dropdown || !document.body.contains(dropdown) || isManual) return; + window.removeEventListener('resize', this.dropdown.position); + this.dropdown.events.binding.call(this, false); // unbind all events + // must delay because if the dropdown is open, and the input (scope) is clicked, + // the dropdown should be now closed, and the next click should re-open it, + // and without this timeout, clicking to close will re-open immediately + + setTimeout(this.events.binding.bind(this), 250); // re-bind main events + + scope.setAttribute("aria-expanded", false); + dropdown.parentNode.removeChild(dropdown); + this.state.dropdown.visible = false; + this.state.ddItemData = null; + this.state.ddItemElm = null; + this.trigger("dropdown:hide", dropdown); + }, + + /** + * fill data into the suggestions list (mainly used to update the list when removing tags, so they will be re-added to the list. not efficient) + */ + refilter: function refilter() { + this.suggestedListItems = this.dropdown.filterListItems.call(this, ''); + var listHTML = this.dropdown.createListHTML.call(this, this.suggestedListItems); + this.DOM.dropdown.content.innerHTML = this.minify(listHTML); + }, + position: function position(ddHeight) { + var isBelowViewport, + rect, + top, + bottom, + left, + width, + ddElm = this.DOM.dropdown; + if (!this.state.dropdown.visible) return; + + if (this.settings.dropdown.position == 'text') { + rect = this.getCaretGlobalPosition(); + bottom = rect.bottom; + top = rect.top; + left = rect.left; + width = 'auto'; + } else { + rect = this.DOM.scope.getBoundingClientRect(); + top = rect.top; + bottom = rect.bottom - 1; + left = rect.left; + width = rect.width + "px"; + } + + top = Math.floor(top); + bottom = Math.ceil(bottom); + isBelowViewport = document.documentElement.clientHeight - bottom < (ddHeight || ddElm.clientHeight); // flip vertically if there is no space for the dropdown below the input + + ddElm.style.cssText = "left:" + (left + window.pageXOffset) + "px; width:" + width + ";" + (isBelowViewport ? "bottom:" + (document.documentElement.clientHeight - top - window.pageYOffset - 2) + "px;" : "top: " + (bottom + window.pageYOffset) + "px"); + ddElm.setAttribute('placement', isBelowViewport ? "top" : "bottom"); + }, + events: { + /** + * Events should only be binded when the dropdown is rendered and removed when isn't + * @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] + */ + binding: function binding() { + var bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; + + // references to the ".bind()" methods must be saved so they could be unbinded later + var _CB = this.dropdown.events.callbacks, + _CBR = this.listeners.dropdown = this.listeners.dropdown || { + position: this.dropdown.position.bind(this), + onKeyDown: _CB.onKeyDown.bind(this), + onMouseOver: _CB.onMouseOver.bind(this), + onMouseLeave: _CB.onMouseLeave.bind(this), + onClick: _CB.onClick.bind(this) + }, + action = bindUnbind ? 'addEventListener' : 'removeEventListener'; + + if (this.settings.dropdown.position != 'manual') { + window[action]('resize', _CBR.position); + window[action]('keydown', _CBR.onKeyDown); + } // window[action]('mousedown', _CBR.onClick); + + + this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); + this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave); + this.DOM.dropdown[action]('mousedown', _CBR.onClick); // add back the main "click" event because it is needed for removing/clicking already-existing tags, even if dropdown is shown + + this.DOM[this.listeners.main.click[0]][action]('click', this.listeners.main.click[1]); + }, + callbacks: { + onKeyDown: function onKeyDown(e) { + // get the "active" element, and if there was none (yet) active, use first child + var activeListElm = this.DOM.dropdown.querySelector("[class$='--active']"), + selectedElm = activeListElm; + + switch (e.key) { + case 'ArrowDown': + case 'ArrowUp': + case 'Down': // >IE11 + + case 'Up': + { + // >IE11 + e.preventDefault(); + var dropdownItems; + if (selectedElm) selectedElm = selectedElm[(e.key == 'ArrowUp' || e.key == 'Up' ? "previous" : "next") + "ElementSibling"]; // if no element was found, loop + + if (!selectedElm) { + dropdownItems = this.DOM.dropdown.content.children; + selectedElm = dropdownItems[e.key == 'ArrowUp' || e.key == 'Up' ? dropdownItems.length - 1 : 0]; + } + + this.dropdown.highlightOption.call(this, selectedElm, true); + break; + } + + case 'Escape': + case 'Esc': + // IE11 + this.dropdown.hide.call(this); + break; + + case 'ArrowRight': + if (this.state.actions.ArrowLeft) return; + + case 'Tab': + { + e.preventDefault(); // in mix-mode, treat arrowRight like Enter key, so a tag will be created + + if (this.settings.mode != 'mix' && !this.settings.autoComplete.rightKey) { + try { + var value = selectedElm ? selectedElm.textContent : this.suggestedListItems[0].value; + this.input.autocomplete.set.call(this, value); + } catch (err) {} + + return false; + } + } + + case 'Enter': + { + e.preventDefault(); + this.dropdown.selectOption.call(this, activeListElm); + break; + } + + case 'Backspace': + { + if (this.settings.mode == 'mix' || this.state.editing.scope) return; + + var _value = this.input.value.trim(); + + if (_value == "" || _value.charCodeAt(0) == 8203) { + if (this.settings.backspace === true) this.removeTag();else if (this.settings.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); + } + } + } + }, + onMouseOver: function onMouseOver(e) { + var ddItem = e.target.closest('.tagify__dropdown__item'); // event delegation check + + ddItem && this.dropdown.highlightOption.call(this, ddItem); + }, + onMouseLeave: function onMouseLeave(e) { + // de-highlight any previously highlighted option + this.dropdown.highlightOption.call(this); + }, + onClick: function onClick(e) { + if (e.button != 0 || e.target == this.DOM.dropdown) return; // allow only mouse left-clicks + + var listItemElm = e.target.closest(".tagify__dropdown__item"); + this.dropdown.selectOption.call(this, listItemElm); + } + } + }, + + /** + * mark the currently active suggestion option + * @param {Object} elm option DOM node + * @param {Boolean} adjustScroll when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element + */ + highlightOption: function highlightOption(elm, adjustScroll) { + var className = "tagify__dropdown__item--active", + itemData; // focus casues a bug in Firefox with the placeholder been shown on the input element + // if( this.settings.dropdown.position != 'manual' ) + // elm.focus(); + + if (this.state.ddItemElm) { + this.state.ddItemElm.classList.remove(className); + this.state.ddItemElm.removeAttribute("aria-selected"); + } + + if (!elm) { + this.state.ddItemData = null; + this.state.ddItemElm = null; + this.input.autocomplete.suggest.call(this); + return; + } + + itemData = this.suggestedListItems[this.getNodeIndex(elm)]; + this.state.ddItemData = itemData; + this.state.ddItemElm = elm; // this.DOM.dropdown.querySelectorAll("[class$='--active']").forEach(activeElm => activeElm.classList.remove(className)); + + elm.classList.add(className); + elm.setAttribute("aria-selected", true); + if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; // Try to autocomplete the typed value with the currently highlighted dropdown item + + if (this.settings.autoComplete) { + this.input.autocomplete.suggest.call(this, itemData); + if (this.settings.dropdown.position != 'manual') this.dropdown.position.call(this); // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line + } + }, + + /** + * Create a tag from the currently active suggestion option + * @param {Object} elm DOM node to select + */ + selectOption: function selectOption(elm) { + var _this13 = this; + + if (!elm) return; // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run + + this.state.actions.selectOption = true; + setTimeout(function () { + return _this13.state.actions.selectOption = false; + }, 50); + var hideDropdown = this.settings.dropdown.closeOnSelect, + value = this.suggestedListItems[this.getNodeIndex(elm)] || this.input.value; + this.trigger("dropdown:select", value); + this.addTags([value], true); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing + + setTimeout(function () { + _this13.DOM.input.focus(); + + _this13.toggleFocusClass(true); + }); + + if (hideDropdown) { + this.dropdown.hide.call(this); // setTimeout(() => this.events.callbacks.onFocusBlur.call(this, {type:"blur"}), 60) + } + }, + + /** + * returns an HTML string of the suggestions' list items + * @param {string} value string to filter the whitelist by + * @return {Array} list of filtered whitelist items according to the settings provided and current value + */ + filterListItems: function filterListItems(value) { + var _this14 = this; + + var _s = this.settings, + list = [], + whitelist = _s.whitelist, + suggestionsCount = _s.dropdown.maxItems || Infinity, + searchKeys = _s.dropdown.searchKeys.concat(["searchBy", "value"]), + whitelistItem, + valueIsInWhitelist, + whitelistItemValueIndex, + searchBy, + isDuplicate, + i = 0; + + if (!value) { + return (_s.duplicates ? whitelist : whitelist.filter(function (item) { + return !_this14.isTagDuplicate(item.value || item); + }) // don't include tags which have already been added. + ).slice(0, suggestionsCount); // respect "maxItems" dropdown setting + } + + for (; i < whitelist.length; i++) { + whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { + value: whitelist[i] + }; //normalize value as an Object + + searchBy = searchKeys.reduce(function (values, k) { + return values + " " + (whitelistItem[k] || ""); + }, "").toLowerCase(); + whitelistItemValueIndex = searchBy.indexOf(value.toLowerCase()); + valueIsInWhitelist = _s.dropdown.fuzzySearch ? whitelistItemValueIndex >= 0 : whitelistItemValueIndex == 0; + isDuplicate = !_s.duplicates && this.isTagDuplicate(whitelistItem.value); // match for the value within each "whitelist" item + + if (valueIsInWhitelist && !isDuplicate && suggestionsCount--) list.push(whitelistItem); + if (suggestionsCount == 0) break; + } + + return list; + }, + + /** + * Creates the dropdown items' HTML + * @param {Array} list [Array of Objects] + * @return {String} + */ + createListHTML: function createListHTML(optionsArr) { + var template = this.settings.templates.dropdownItem.bind(this); + return this.minify(optionsArr.map(template).join("")); + } + } +}; +return Tagify; +})); diff --git a/server/user.jobengine.osgi.server/js/tagify.min.js b/server/user.jobengine.osgi.server/js/tagify.min.js new file mode 100644 index 00000000..24c19a9b --- /dev/null +++ b/server/user.jobengine.osgi.server/js/tagify.min.js @@ -0,0 +1,7 @@ +/** + * Tagify (v 3.2.6)- tags input component + * By Yair Even-Or + * Don't sell this code. (c) + * https://github.com/yairEO/tagify + */ +!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Tagify=e()}(this,function(){"use strict";function u(t){return function(t){if(Array.isArray(t)){for(var e=0,i=new Array(t.length);e