diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/colors/macos.json b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/macos.json new file mode 100644 index 00000000..a32cb695 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/macos.json @@ -0,0 +1 @@ +[[1,1,1],[1,1,0.8],[1,1,0.6],[1,1,0.4],[1,1,0.2],[1,1,0],[1,0.8,1],[1,0.8,0.8],[1,0.8,0.6],[1,0.8,0.4],[1,0.8,0.2],[1,0.8,0],[1,0.6,1],[1,0.6,0.8],[1,0.6,0.6],[1,0.6,0.4],[1,0.6,0.2],[1,0.6,0],[1,0.4,1],[1,0.4,0.8],[1,0.4,0.6],[1,0.4,0.4],[1,0.4,0.2],[1,0.4,0],[1,0.2,1],[1,0.2,0.8],[1,0.2,0.6],[1,0.2,0.4],[1,0.2,0.2],[1,0.2,0],[1,0,1],[1,0,0.8],[1,0,0.6],[1,0,0.4],[1,0,0.2],[1,0,0],[0.8,1,1],[0.8,1,0.8],[0.8,1,0.6],[0.8,1,0.4],[0.8,1,0.2],[0.8,1,0],[0.8,0.8,1],[0.8,0.8,0.8],[0.8,0.8,0.6],[0.8,0.8,0.4],[0.8,0.8,0.2],[0.8,0.8,0],[0.8,0.6,1],[0.8,0.6,0.8],[0.8,0.6,0.6],[0.8,0.6,0.4],[0.8,0.6,0.2],[0.8,0.6,0],[0.8,0.4,1],[0.8,0.4,0.8],[0.8,0.4,0.6],[0.8,0.4,0.4],[0.8,0.4,0.2],[0.8,0.4,0],[0.8,0.2,1],[0.8,0.2,0.8],[0.8,0.2,0.6],[0.8,0.2,0.4],[0.8,0.2,0.2],[0.8,0.2,0],[0.8,0,1],[0.8,0,0.8],[0.8,0,0.6],[0.8,0,0.4],[0.8,0,0.2],[0.8,0,0],[0.6,1,1],[0.6,1,0.8],[0.6,1,0.6],[0.6,1,0.4],[0.6,1,0.2],[0.6,1,0],[0.6,0.8,1],[0.6,0.8,0.8],[0.6,0.8,0.6],[0.6,0.8,0.4],[0.6,0.8,0.2],[0.6,0.8,0],[0.6,0.6,1],[0.6,0.6,0.8],[0.6,0.6,0.6],[0.6,0.6,0.4],[0.6,0.6,0.2],[0.6,0.6,0],[0.6,0.4,1],[0.6,0.4,0.8],[0.6,0.4,0.6],[0.6,0.4,0.4],[0.6,0.4,0.2],[0.6,0.4,0],[0.6,0.2,1],[0.6,0.2,0.8],[0.6,0.2,0.6],[0.6,0.2,0.4],[0.6,0.2,0.2],[0.6,0.2,0],[0.6,0,1],[0.6,0,0.8],[0.6,0,0.6],[0.6,0,0.4],[0.6,0,0.2],[0.6,0,0],[0.4,1,1],[0.4,1,0.8],[0.4,1,0.6],[0.4,1,0.4],[0.4,1,0.2],[0.4,1,0],[0.4,0.8,1],[0.4,0.8,0.8],[0.4,0.8,0.6],[0.4,0.8,0.4],[0.4,0.8,0.2],[0.4,0.8,0],[0.4,0.6,1],[0.4,0.6,0.8],[0.4,0.6,0.6],[0.4,0.6,0.4],[0.4,0.6,0.2],[0.4,0.6,0],[0.4,0.4,1],[0.4,0.4,0.8],[0.4,0.4,0.6],[0.4,0.4,0.4],[0.4,0.4,0.2],[0.4,0.4,0],[0.4,0.2,1],[0.4,0.2,0.8],[0.4,0.2,0.6],[0.4,0.2,0.4],[0.4,0.2,0.2],[0.4,0.2,0],[0.4,0,1],[0.4,0,0.8],[0.4,0,0.6],[0.4,0,0.4],[0.4,0,0.2],[0.4,0,0],[0.2,1,1],[0.2,1,0.8],[0.2,1,0.6],[0.2,1,0.4],[0.2,1,0.2],[0.2,1,0],[0.2,0.8,1],[0.2,0.8,0.8],[0.2,0.8,0.6],[0.2,0.8,0.4],[0.2,0.8,0.2],[0.2,0.8,0],[0.2,0.6,1],[0.2,0.6,0.8],[0.2,0.6,0.6],[0.2,0.6,0.4],[0.2,0.6,0.2],[0.2,0.6,0],[0.2,0.4,1],[0.2,0.4,0.8],[0.2,0.4,0.6],[0.2,0.4,0.4],[0.2,0.4,0.2],[0.2,0.4,0],[0.2,0.2,1],[0.2,0.2,0.8],[0.2,0.2,0.6],[0.2,0.2,0.4],[0.2,0.2,0.2],[0.2,0.2,0],[0.2,0,1],[0.2,0,0.8],[0.2,0,0.6],[0.2,0,0.4],[0.2,0,0.2],[0.2,0,0],[0,1,1],[0,1,0.8],[0,1,0.6],[0,1,0.4],[0,1,0.2],[0,1,0],[0,0.8,1],[0,0.8,0.8],[0,0.8,0.6],[0,0.8,0.4],[0,0.8,0.2],[0,0.8,0],[0,0.6,1],[0,0.6,0.8],[0,0.6,0.6],[0,0.6,0.4],[0,0.6,0.2],[0,0.6,0],[0,0.4,1],[0,0.4,0.8],[0,0.4,0.6],[0,0.4,0.4],[0,0.4,0.2],[0,0.4,0],[0,0.2,1],[0,0.2,0.8],[0,0.2,0.6],[0,0.2,0.4],[0,0.2,0.2],[0,0.2,0],[0,0,1],[0,0,0.8],[0,0,0.6],[0,0,0.4],[0,0,0.2],[0.9333333333333333,0,0],[0.8666666666666667,0,0],[0.7333333333333333,0,0],[0.6666666666666666,0,0],[0.5333333333333333,0,0],[0.4666666666666667,0,0],[0.3333333333333333,0,0],[0.26666666666666666,0,0],[0.13333333333333333,0,0],[0.06666666666666667,0,0],[0,0.9333333333333333,0],[0,0.8666666666666667,0],[0,0.7333333333333333,0],[0,0.6666666666666666,0],[0,0.5333333333333333,0],[0,0.4666666666666667,0],[0,0.3333333333333333,0],[0,0.26666666666666666,0],[0,0.13333333333333333,0],[0,0.06666666666666667,0],[0,0,0.9333333333333333],[0,0,0.8666666666666667],[0,0,0.7333333333333333],[0,0,0.6666666666666666],[0,0,0.5333333333333333],[0,0,0.4666666666666667],[0,0,0.3333333333333333],[0,0,0.26666666666666666],[0,0,0.13333333333333333],[0,0,0.06666666666666667],[0.9333333333333333,0.9333333333333333,0.9333333333333333],[0.8666666666666667,0.8666666666666667,0.8666666666666667],[0.7333333333333333,0.7333333333333333,0.7333333333333333],[0.6666666666666666,0.6666666666666666,0.6666666666666666],[0.5333333333333333,0.5333333333333333,0.5333333333333333],[0.4666666666666667,0.4666666666666667,0.4666666666666667],[0.3333333333333333,0.3333333333333333,0.3333333333333333],[0.26666666666666666,0.26666666666666666,0.26666666666666666],[0.13333333333333333,0.13333333333333333,0.13333333333333333],[0.06666666666666667,0.06666666666666667,0.06666666666666667],[0,0,0]] diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/colors/web.json b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/web.json new file mode 100644 index 00000000..7699db0b --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/web.json @@ -0,0 +1 @@ +[[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,0.8],[0.8,1,1],[1,0.6,0.6],[1,0.6,0.6],[1,0.6,0.6],[1,0.6,0.6],[1,0.8,0.6],[1,1,0.6],[0.8,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.6],[0.6,1,0.8],[0.6,1,1],[1,0.4,0.4],[1,0.4,0.4],[1,0.4,0.4],[1,0.6,0.4],[1,0.8,0.4],[1,1,0.4],[0.8,1,0.4],[0.6,1,0.4],[0.4,1,0.4],[0.4,1,0.4],[0.4,1,0.4],[0.4,1,0.4],[0.4,1,0.4],[0.4,1,0.6],[0.4,1,0.8],[0.4,1,1],[1,0.2,0.2],[1,0.2,0.2],[1,0.4,0.2],[1,0.6,0.2],[1,0.8,0.2],[1,1,0.2],[0.8,1,0.2],[0.6,1,0.2],[0.4,1,0.2],[0.2,1,0.2],[0.2,1,0.2],[0.2,1,0.2],[0.2,1,0.4],[0.2,1,0.6],[0.2,1,0.8],[0.2,1,1],[1,0,0],[1,0.2,0],[1,0.4,0],[1,0.6,0],[1,0.8,0],[1,1,0],[0.8,1,0],[0.6,1,0],[0.2,1,0],[0.2,1,0],[0,1,0],[0,1,0.2],[0,1,0.4],[0,1,0.6],[0,1,0.8],[0,1,1],[0.8,0,0],[0.8,0.2,0],[0.8,0.4,0],[0.8,0.6,0],[0.8,0.8,0],[0.8,0.8,0],[0.8,0.8,0],[0.6,0.8,0],[0.4,0.8,0],[0.2,0.8,0],[0,0.8,0],[0,0.8,0.2],[0,0.8,0.4],[0,0.8,0.6],[0,0.8,0.8],[0,0.8,0.8],[0.6,0,0],[0.6,0.2,0],[0.6,0.4,0],[0.6,0.6,0],[0.6,0.6,0],[0.6,0.6,0],[0.6,0.6,0],[0.6,0.6,0],[0.4,0.6,0],[0.2,0.6,0],[0,0.6,0],[0,0.6,0.2],[0,0.6,0.4],[0,0.6,0.6],[0,0.6,0.6],[0,0.6,0.6],[0.4,0,0],[0.4,0.2,0],[0.4,0.4,0],[0.4,0.4,0],[0.4,0.4,0],[0.4,0.4,0],[0.4,0.4,0],[0.4,0.4,0],[0.4,0.4,0],[0.2,0.4,0],[0,0.4,0],[0,0.4,0.2],[0,0.4,0.4],[0,0.4,0.4],[0,0.4,0.4],[0,0.4,0.4],[0.2,0,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0.2,0.2,0],[0,0.2,0],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0.2,0,0],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0.2,0,0.2],[0,0,0.2],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0,0.2,0.2],[0.4,0,0],[0.4,0,0.2],[0.4,0,0.4],[0.4,0,0.4],[0.4,0,0.4],[0.4,0,0.4],[0.4,0,0.4],[0.4,0,0.4],[0.4,0,0.4],[0.2,0,0.4],[0,0,0.4],[0,0.2,0.4],[0,0.4,0.4],[0,0.4,0.4],[0,0.4,0.4],[0,0.4,0.4],[0.6,0,0],[0.6,0,0.2],[0.6,0,0.4],[0.6,0,0.6],[0.6,0,0.6],[0.6,0,0.6],[0.6,0,0.6],[0.6,0,0.6],[0.4,0,0.6],[0.2,0,0.6],[0,0,0.6],[0,0.2,0.6],[0,0.4,0.6],[0,0.6,0.6],[0,0.6,0.6],[0,0.6,0.6],[0.8,0,0],[0.8,0,0.2],[0.8,0,0.4],[0.8,0,0.6],[0.8,0,0.8],[0.8,0,0.8],[0.8,0,0.8],[0.6,0,0.8],[0.4,0,0.8],[0.2,0,0.8],[0,0,0.8],[0,0.2,0.8],[0,0.4,0.8],[0,0.6,0.8],[0,0.8,0.8],[0,0.8,0.8],[1,0,0],[1,0,0.2],[1,0,0.4],[1,0,0.6],[1,0,0.8],[1,0,1],[0.8,0,1],[0.6,0,1],[0.4,0,1],[0.2,0,1],[0,0,1],[0,0.2,1],[0,0.4,1],[0,0.6,1],[0,0.8,1],[0,1,1],[1,0.2,0.2],[1,0.2,0.2],[1,0.2,0.4],[1,0.2,0.6],[1,0.2,0.8],[1,0.2,1],[0.8,0.2,1],[0.6,0.2,1],[0.4,0.2,1],[0.2,0.2,1],[0.2,0.2,1],[0.2,0.2,1],[0.2,0.4,1],[0.2,0.6,1],[0.2,0.8,1],[0.2,1,1],[1,0.4,0.4],[1,0.4,0.4],[1,0.4,0.4],[1,0.4,0.6],[1,0.4,0.8],[1,0.4,1],[0.8,0.4,1],[0.6,0.4,1],[0.4,0.4,1],[0.4,0.4,1],[0.4,0.4,1],[0.4,0.4,1],[0.4,0.4,1],[0.4,0.6,1],[0.4,0.8,1],[0.4,1,1],[1,0.6,0.6],[1,0.6,0.6],[1,0.6,0.6],[1,0.6,0.6],[1,0.6,0.8],[1,0.6,1],[0.8,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.6,1],[0.6,0.8,1],[0.6,1,1],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,0.8],[1,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,0.8,1],[0.8,1,1],[0,0,0],[0.2,0.2,0.2],[0.4,0.4,0.4],[0.6,0.6,0.6],[0.8,0.8,0.8],[1,1,1],[0.8,0.6,0.6],[0.8,0.4,0.4],[0.8,0.2,0.2],[0.6,0.2,0.2],[0.4,0.2,0.2],[0.8,0.4,0.2],[0.8,0.4,0.2],[0.8,0.6,0.4],[0.8,0.6,0.2],[0.8,0.8,0.2],[0.6,0.6,0.4],[0.6,0.6,0.2],[0.8,0.8,0.4],[0.6,0.8,0.4],[0.4,0.6,0.2],[0.4,0.8,0.2],[0.4,0.8,0.4],[0.4,0.6,0.4],[0.4,0.6,0.4],[0.2,0.6,0.2],[0.2,0.4,0.2],[0.2,0.8,0.4],[0.2,0.8,0.4],[0.2,0.8,0.6],[0.2,0.8,0.6],[0.2,0.8,0.8],[0.6,0.8,0.8],[0.2,0.6,0.6],[0.4,0.8,0.8],[0.2,0.4,0.4],[0.2,0.4,0.6],[0.4,0.4,0.6],[0.4,0.4,0.8],[0.4,0.4,0.8],[0.2,0.2,0.6],[0.4,0.2,0.8],[0.4,0.2,0.6],[0.6,0.4,0.8],[0.6,0.2,0.8],[0.8,0.6,0.8],[0.4,0.2,0.4],[0.6,0.4,0.6],[0.8,0.2,0.8],[0.8,0.2,0.6],[0.8,0.4,0.6],[0.6,0.2,0.4],[0.8,0.2,0.4]] diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/colors/windows.json b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/windows.json new file mode 100644 index 00000000..fdc4eb12 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/colors/windows.json @@ -0,0 +1 @@ +[[0,0,0],[0.5019607843137255,0,0],[0,0.5019607843137255,0],[0.5019607843137255,0.5019607843137255,0],[0,0,0.5019607843137255],[0.5019607843137255,0,0.5019607843137255],[0,0.5019607843137255,0.5019607843137255],[0.5019607843137255,0.5019607843137255,0.5019607843137255],[0.7529411764705882,0.8627450980392157,0.7529411764705882],[0.6509803921568628,0.792156862745098,0.9411764705882353],[0.16470588235294117,0.24705882352941178,0.6666666666666666],[0.16470588235294117,0.24705882352941178,1],[0.16470588235294117,0.37254901960784315,0],[0.16470588235294117,0.37254901960784315,0.3333333333333333],[0.16470588235294117,0.37254901960784315,0.6666666666666666],[0.16470588235294117,0.37254901960784315,1],[0.16470588235294117,0.4980392156862745,0],[0.16470588235294117,0.4980392156862745,0.3333333333333333],[0.16470588235294117,0.4980392156862745,0.6666666666666666],[0.16470588235294117,0.4980392156862745,1],[0.16470588235294117,0.6235294117647059,0],[0.16470588235294117,0.6235294117647059,0.3333333333333333],[0.16470588235294117,0.6235294117647059,0.6666666666666666],[0.16470588235294117,0.6235294117647059,1],[0.16470588235294117,0.7490196078431373,0],[0.16470588235294117,0.7490196078431373,0.3333333333333333],[0.16470588235294117,0.7490196078431373,0.6666666666666666],[0.16470588235294117,0.7490196078431373,1],[0.16470588235294117,0.8745098039215686,0],[0.16470588235294117,0.8745098039215686,0.3333333333333333],[0.16470588235294117,0.8745098039215686,0.6666666666666666],[0.16470588235294117,0.8745098039215686,1],[0.16470588235294117,1,0],[0.16470588235294117,1,0.3333333333333333],[0.16470588235294117,1,0.6666666666666666],[0.16470588235294117,1,1],[0.3333333333333333,0,0],[0.3333333333333333,0,0.3333333333333333],[0.3333333333333333,0,0.6666666666666666],[0.3333333333333333,0,1],[0.3333333333333333,0.12156862745098039,0],[0.3333333333333333,0.12156862745098039,0.3333333333333333],[0.3333333333333333,0.12156862745098039,0.6666666666666666],[0.3333333333333333,0.12156862745098039,1],[0.3333333333333333,0.24705882352941178,0],[0.3333333333333333,0.24705882352941178,0.3333333333333333],[0.3333333333333333,0.24705882352941178,0.6666666666666666],[0.3333333333333333,0.24705882352941178,1],[0.3333333333333333,0.37254901960784315,0],[0.3333333333333333,0.37254901960784315,0.3333333333333333],[0.3333333333333333,0.37254901960784315,0.6666666666666666],[0.3333333333333333,0.37254901960784315,1],[0.3333333333333333,0.4980392156862745,0],[0.3333333333333333,0.4980392156862745,0.3333333333333333],[0.3333333333333333,0.4980392156862745,0.6666666666666666],[0.3333333333333333,0.4980392156862745,1],[0.3333333333333333,0.6235294117647059,0],[0.3333333333333333,0.6235294117647059,0.3333333333333333],[0.3333333333333333,0.6235294117647059,0.6666666666666666],[0.3333333333333333,0.6235294117647059,1],[0.3333333333333333,0.7490196078431373,0],[0.3333333333333333,0.7490196078431373,0.3333333333333333],[0.3333333333333333,0.7490196078431373,0.6666666666666666],[0.3333333333333333,0.7490196078431373,1],[0.3333333333333333,0.8745098039215686,0],[0.3333333333333333,0.8745098039215686,0.3333333333333333],[0.3333333333333333,0.8745098039215686,0.6666666666666666],[0.3333333333333333,0.8745098039215686,1],[0.3333333333333333,1,0],[0.3333333333333333,1,0.3333333333333333],[0.3333333333333333,1,0.6666666666666666],[0.3333333333333333,1,1],[0.4980392156862745,0,0],[0.4980392156862745,0,0.3333333333333333],[0.4980392156862745,0,0.6666666666666666],[0.4980392156862745,0,1],[0.4980392156862745,0.12156862745098039,0],[0.4980392156862745,0.12156862745098039,0.3333333333333333],[0.4980392156862745,0.12156862745098039,0.6666666666666666],[0.4980392156862745,0.12156862745098039,1],[0.4980392156862745,0.24705882352941178,0],[0.4980392156862745,0.24705882352941178,0.3333333333333333],[0.4980392156862745,0.24705882352941178,0.6666666666666666],[0.4980392156862745,0.24705882352941178,1],[0.4980392156862745,0.37254901960784315,0],[0.4980392156862745,0.37254901960784315,0.3333333333333333],[0.4980392156862745,0.37254901960784315,0.6666666666666666],[0.4980392156862745,0.37254901960784315,1],[0.4980392156862745,0.4980392156862745,0],[0.4980392156862745,0.4980392156862745,0.3333333333333333],[0.4980392156862745,0.4980392156862745,0.6666666666666666],[0.4980392156862745,0.4980392156862745,1],[0.4980392156862745,0.6235294117647059,0],[0.4980392156862745,0.6235294117647059,0.3333333333333333],[0.4980392156862745,0.6235294117647059,0.6666666666666666],[0.4980392156862745,0.6235294117647059,1],[0.4980392156862745,0.7490196078431373,0],[0.4980392156862745,0.7490196078431373,0.3333333333333333],[0.4980392156862745,0.7490196078431373,0.6666666666666666],[0.4980392156862745,0.7490196078431373,1],[0.4980392156862745,0.8745098039215686,0],[0.4980392156862745,0.8745098039215686,0.3333333333333333],[0.4980392156862745,0.8745098039215686,0.6666666666666666],[0.4980392156862745,0.8745098039215686,1],[0.4980392156862745,1,0],[0.4980392156862745,1,0.3333333333333333],[0.4980392156862745,1,0.6666666666666666],[0.4980392156862745,1,1],[0.6666666666666666,0,0],[0.6666666666666666,0,0.3333333333333333],[0.6666666666666666,0,0.6666666666666666],[0.6666666666666666,0,1],[0.6666666666666666,0.12156862745098039,0],[0.6666666666666666,0.12156862745098039,0.3333333333333333],[0.6666666666666666,0.12156862745098039,0.6666666666666666],[0.6666666666666666,0.12156862745098039,1],[0.6666666666666666,0.24705882352941178,0],[0.6666666666666666,0.24705882352941178,0.3333333333333333],[0.6666666666666666,0.24705882352941178,0.6666666666666666],[0.6666666666666666,0.24705882352941178,1],[0.6666666666666666,0.37254901960784315,0],[0.6666666666666666,0.37254901960784315,0.3333333333333333],[0.6666666666666666,0.37254901960784315,0.6666666666666666],[0.6666666666666666,0.37254901960784315,1],[0.6666666666666666,0.4980392156862745,0],[0.6666666666666666,0.4980392156862745,0.3333333333333333],[0.6666666666666666,0.4980392156862745,0.6666666666666666],[0.6666666666666666,0.4980392156862745,1],[0.6666666666666666,0.6235294117647059,0],[0.6666666666666666,0.6235294117647059,0.3333333333333333],[0.6666666666666666,0.6235294117647059,0.6666666666666666],[0.6666666666666666,0.6235294117647059,1],[0.6666666666666666,0.7490196078431373,0],[0.6666666666666666,0.7490196078431373,0.3333333333333333],[0.6666666666666666,0.7490196078431373,0.6666666666666666],[0.6666666666666666,0.7490196078431373,1],[0.6666666666666666,0.8745098039215686,0],[0.6666666666666666,0.8745098039215686,0.3333333333333333],[0.6666666666666666,0.8745098039215686,0.6666666666666666],[0.6666666666666666,0.8745098039215686,1],[0.6666666666666666,1,0],[0.6666666666666666,1,0.3333333333333333],[0.6666666666666666,1,0.6666666666666666],[0.6666666666666666,1,1],[0.8313725490196079,0,0],[0.8313725490196079,0,0.3333333333333333],[0.8313725490196079,0,0.6666666666666666],[0.8313725490196079,0,1],[0.8313725490196079,0.12156862745098039,0],[0.8313725490196079,0.12156862745098039,0.3333333333333333],[0.8313725490196079,0.12156862745098039,0.6666666666666666],[0.8313725490196079,0.12156862745098039,1],[0.8313725490196079,0.24705882352941178,0],[0.8313725490196079,0.24705882352941178,0.3333333333333333],[0.8313725490196079,0.24705882352941178,0.6666666666666666],[0.8313725490196079,0.24705882352941178,1],[0.8313725490196079,0.37254901960784315,0],[0.8313725490196079,0.37254901960784315,0.3333333333333333],[0.8313725490196079,0.37254901960784315,0.6666666666666666],[0.8313725490196079,0.37254901960784315,1],[0.8313725490196079,0.4980392156862745,0],[0.8313725490196079,0.4980392156862745,0.3333333333333333],[0.8313725490196079,0.4980392156862745,0.6666666666666666],[0.8313725490196079,0.4980392156862745,1],[0.8313725490196079,0.6235294117647059,0],[0.8313725490196079,0.6235294117647059,0.3333333333333333],[0.8313725490196079,0.6235294117647059,0.6666666666666666],[0.8313725490196079,0.6235294117647059,1],[0.8313725490196079,0.7490196078431373,0],[0.8313725490196079,0.7490196078431373,0.3333333333333333],[0.8313725490196079,0.7490196078431373,0.6666666666666666],[0.8313725490196079,0.7490196078431373,1],[0.8313725490196079,0.8745098039215686,0],[0.8313725490196079,0.8745098039215686,0.3333333333333333],[0.8313725490196079,0.8745098039215686,0.6666666666666666],[0.8313725490196079,0.8745098039215686,1],[0.8313725490196079,1,0],[0.8313725490196079,1,0.3333333333333333],[0.8313725490196079,1,0.6666666666666666],[0.8313725490196079,1,1],[1,0,0.3333333333333333],[1,0,0.6666666666666666],[1,0.12156862745098039,0],[1,0.12156862745098039,0.3333333333333333],[1,0.12156862745098039,0.6666666666666666],[1,0.12156862745098039,1],[1,0.24705882352941178,0],[1,0.24705882352941178,0.3333333333333333],[1,0.24705882352941178,0.6666666666666666],[1,0.24705882352941178,1],[1,0.37254901960784315,0],[1,0.37254901960784315,0.3333333333333333],[1,0.37254901960784315,0.6666666666666666],[1,0.37254901960784315,1],[1,0.4980392156862745,0],[1,0.4980392156862745,0.3333333333333333],[1,0.4980392156862745,0.6666666666666666],[1,0.4980392156862745,1],[1,0.6235294117647059,0],[1,0.6235294117647059,0.3333333333333333],[1,0.6235294117647059,0.6666666666666666],[1,0.6235294117647059,1],[1,0.7490196078431373,0],[1,0.7490196078431373,0.3333333333333333],[1,0.7490196078431373,0.6666666666666666],[1,0.7490196078431373,1],[1,0.8745098039215686,0],[1,0.8745098039215686,0.3333333333333333],[1,0.8745098039215686,0.6666666666666666],[1,0.8745098039215686,1],[1,1,0.3333333333333333],[1,1,0.6666666666666666],[0.8,0.8,1],[1,0.8,1],[0.2,1,1],[0.4,1,1],[0.6,1,1],[0.8,1,1],[0,0.4980392156862745,0],[0,0.4980392156862745,0.3333333333333333],[0,0.4980392156862745,0.6666666666666666],[0,0.4980392156862745,1],[0,0.6235294117647059,0],[0,0.6235294117647059,0.3333333333333333],[0,0.6235294117647059,0.6666666666666666],[0,0.6235294117647059,1],[0,0.7490196078431373,0],[0,0.7490196078431373,0.3333333333333333],[0,0.7490196078431373,0.6666666666666666],[0,0.7490196078431373,1],[0,0.8745098039215686,0],[0,0.8745098039215686,0.3333333333333333],[0,0.8745098039215686,0.6666666666666666],[0,0.8745098039215686,1],[0,1,0.3333333333333333],[0,1,0.6666666666666666],[0.16470588235294117,0,0],[0.16470588235294117,0,0.3333333333333333],[0.16470588235294117,0,0.6666666666666666],[0.16470588235294117,0,1],[0.16470588235294117,0.12156862745098039,0],[0.16470588235294117,0.12156862745098039,0.3333333333333333],[0.16470588235294117,0.12156862745098039,0.6666666666666666],[0.16470588235294117,0.12156862745098039,1],[0.16470588235294117,0.24705882352941178,0],[0.16470588235294117,0.24705882352941178,0.3333333333333333],[1,0.984313725490196,0.9411764705882353],[0.6274509803921569,0.6274509803921569,0.6431372549019608],[0.5019607843137255,0.5019607843137255,0.5019607843137255],[1,0,0],[0,1,0],[1,1,0],[0,0,1],[1,0,1],[0,1,1],[1,1,1]] diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/es6-promise.auto.min.js b/src/main/webapp/resources/lib/fims/biz/paintweb/es6-promise.auto.min.js new file mode 100644 index 00000000..fdf8bff2 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/es6-promise.auto.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){B=t}function r(t){G=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof z?function(){z(a)}:c()}function s(){var t=0,e=new J(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t. + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-09 14:26:21 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the implementation of the Color Mixer dialog. + */ + +// For the implementation of this extension I used the following references: +// - Wikipedia articles on each subject. +// - the great brucelindbloom.com Web site - lots of information. + +/** + * @class The Color Mixer extension. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.extensions.colormixer = function (app) { + var _self = this, + config = app.config.colormixer, + doc = app.doc, + gui = app.gui, + lang = app.lang.colormixer, + MathFloor = Math.floor, + MathMax = Math.max, + MathMin = Math.min, + MathPow = Math.pow, + MathRound = Math.round, + resScale = app.resolution.scale; + + /** + * Holds references to various DOM elements. + * + * @private + * @type Object + */ + this.elems = { + /** + * Reference to the element which holds Canvas controls (the dot on the + * Canvas, and the slider). + * @type Element + */ + 'controls': null, + + /** + * Reference to the dot element that is rendered on top of the color space + * visualisation. + * @type Element + */ + 'chartDot': null, + + /** + * Reference to the slider element. + * @type Element + */ + 'slider': null, + + /** + * Reference to the input element that allows the user to pick the color + * palette to be displayed. + * @type Element + */ + 'cpaletteInput': null, + + /** + * The container element which holds the colors of the currently selected + * palette. + * @type Element + */ + 'cpaletteOutput': null, + + /** + * Reference to the element which displays the current color. + * @type Element + */ + "colorActive": null, + + /** + * Reference to the element which displays the old color. + * @type Element + */ + "colorOld": null + }; + + /** + * Reference to the Color Mixer floating panel GUI component object. + * + * @private + * @type pwlib.guiFloatingPanel + */ + this.panel = null; + + /** + * Reference to the Color Mixer tab panel GUI component object which holds the + * inputs. + * + * @private + * @type pwlib.guiTabPanel + */ + this.panelInputs = null; + + /** + * Reference to the Color Mixer tab panel GUI component object which holds the + * Canvas used for color space visualisation and the color palettes selector. + * + * @private + * @type pwlib.guiTabPanel + */ + this.panelSelector = null; + + /** + * Holds a reference to the 2D context of the color mixer Canvas element. This + * is where the color chart and the slider are both drawn. + * + * @private + * @type CanvasRenderingContext2D + */ + this.context2d = false; + + /** + * Target input hooks. This object must hold two methods: + * + * + * + *

The object must also hold information about the associated configuration + * property: configProperty, configGroup and + * configGroupRef. + * + * @type Object + */ + this.targetInput = null; + + /** + * Holds the current color in several formats: RGB, HEX, HSV, CIE Lab, and + * CMYK. Except for 'hex', all the values should be from 0 to 1. + * + * @type Object + */ + this.color = { + // RGB + red : 0, + green: 0, + blue : 0, + + alpha : 0, + hex : 0, + + // HSV + hue : 0, + sat : 0, + val : 0, + + // CMYK + cyan : 0, + magenta : 0, + yellow : 0, + black : 0, + + // CIE Lab + cie_l : 0, + cie_a : 0, + cie_b : 0 + }; + + /** + * Holds references to all the DOM input fields, for each color channel. + * + * @private + * @type Object + */ + this.inputs = { + red : null, + green : null, + blue : null, + + alpha : null, + hex : null, + + hue : null, + sat : null, + val : null, + + cyan : null, + magenta : null, + yellow : null, + black : null, + + cie_l : null, + cie_a : null, + cie_b : null + }; + + /** + * The "absolute maximum" value is determined based on the min/max values. + * For min -100 and max 100, the abs_max is 200. + * @private + * + */ + this.abs_max = {}; + + // The hue spectrum used by the HSV charts. + var hueSpectrum = [ + [255, 0, 0], // 0, Red, 0° + [255, 255, 0], // 1, Yellow, 60° + [ 0, 255, 0], // 2, Green, 120° + [ 0, 255, 255], // 3, Cyan, 180° + [ 0, 0, 255], // 4, Blue, 240° + [255, 0, 255], // 5, Magenta, 300° + [255, 0, 0] // 6, Red, 360° + ]; + + // The active color key (input) determines how the color chart works. + this.ckey_active = 'red'; + + // Given a group of the inputs: red, green and blue, when one of them is active, the ckey_adjoint is set to an array of the other two input IDs. + this.ckey_adjoint = false; + this.ckey_active_group = false; + + this.ckey_grouping = { + 'red' : 'rgb', + 'green' : 'rgb', + 'blue' : 'rgb', + + 'hue' : 'hsv', + 'sat' : 'hsv', + 'val' : 'hsv', + + 'cyan' : 'cmyk', + 'magenta' : 'cmyk', + 'yellow' : 'cmyk', + 'black' : 'cmyk', + + 'cie_l' : 'lab', + 'cie_a' : 'lab', + 'cie_b' : 'lab' + }; + + // These values are automatically calculated when the color mixer is + // initialized. + this.sliderX = 0; + this.sliderWidth = 0; + this.sliderHeight = 0; + this.sliderSpacing = 0; + this.chartWidth = 0; + this.chartHeight = 0; + + /** + * Register the Color Mixer extension. + * + * @returns {Boolean} True if the extension can be registered properly, or + * false if not. + */ + this.extensionRegister = function (ev) { + if (!gui.elems || !gui.elems.colormixer_canvas || !gui.floatingPanels || + !gui.floatingPanels.colormixer || !gui.tabPanels || + !gui.tabPanels.colormixer_inputs || !gui.tabPanels.colormixer_selector + || !_self.init_lab()) { + return false; + } + + _self.panel = gui.floatingPanels.colormixer; + _self.panelSelector = gui.tabPanels.colormixer_selector; + _self.panelInputs = gui.tabPanels.colormixer_inputs; + + // Initialize the color mixer Canvas element. + _self.context2d = gui.elems.colormixer_canvas.getContext('2d'); + if (!_self.context2d) { + return false; + } + + // Setup the color mixer inputs. + var elem, label, labelElem, + inputValues = config.inputValues, + form = _self.panelInputs.container; + if (!form) { + return false; + } + + for (var i in _self.inputs) { + elem = form.elements.namedItem('ckey_' + i) || gui.inputs['ckey_' + i]; + if (!elem) { + return false; + } + + if (i === 'hex' || i === 'alpha') { + label = lang.inputs[i]; + } else { + label = lang.inputs[_self.ckey_grouping[i] + '_' + i]; + } + + labelElem = elem.parentNode; + labelElem.replaceChild(doc.createTextNode(label), labelElem.firstChild); + + elem.addEventListener('input', _self.ev_input_change, false); + elem.addEventListener('change', _self.ev_input_change, false); + + if (i !== 'hex') { + elem.setAttribute('step', inputValues[i][2]); + + elem.setAttribute('max', MathRound(inputValues[i][1])); + elem.setAttribute('min', MathRound(inputValues[i][0])); + _self.abs_max[i] = inputValues[i][1] - inputValues[i][0]; + } + + // Store the color key, which is used by the event handler. + elem._ckey = i; + _self.inputs[i] = elem; + } + + // Setup the ckey inputs of type=radio. + var ckey = form.ckey; + if (!ckey) { + return false; + } + for (var i = 0, n = ckey.length; i < n; i++) { + elem = ckey[i]; + if (_self.ckey_grouping[elem.value] === 'lab' && + !_self.context2d.putImageData) { + elem.disabled = true; + continue; + } + + elem.addEventListener('change', _self.ev_change_ckey_active, false); + + if (elem.value === _self.ckey_active) { + elem.checked = true; + _self.update_ckey_active(_self.ckey_active, true); + } + } + + // Prepare the color preview elements. + _self.elems.colorActive = gui.elems.colormixer_colorActive.firstChild; + _self.elems.colorOld = gui.elems.colormixer_colorOld.firstChild; + _self.elems.colorOld.addEventListener('click', _self.ev_click_color, false); + + // Make sure the buttons work properly. + var anchor, btn, buttons = ['accept', 'cancel', 'saveColor', 'pickColor']; + for (var i = 0, n = buttons.length; i < n; i++) { + btn = gui.elems['colormixer_btn_' + buttons[i]]; + if (!btn) { + continue; + } + + anchor = doc.createElement('a'); + anchor.href = '#'; + anchor.appendChild(doc.createTextNode(lang.buttons[buttons[i]])); + anchor.addEventListener('click', _self['ev_click_' + buttons[i]], false); + + btn.replaceChild(anchor, btn.firstChild); + } + + // Prepare the canvas "controls" (the chart "dot" and the slider). + var id, elems = ['controls', 'chartDot', 'slider']; + for (var i = 0, n = elems.length; i < n; i++) { + id = elems[i]; + elem = gui.elems['colormixer_' + id]; + if (!elem) { + return false; + } + + elem.addEventListener('mousedown', _self.ev_canvas, false); + elem.addEventListener('mousemove', _self.ev_canvas, false); + elem.addEventListener('mouseup', _self.ev_canvas, false); + + _self.elems[id] = elem; + } + + // The color palette . + var palette; + for (var i in config.colorPalettes) { + palette = config.colorPalettes[i]; + elem = doc.createElement('option'); + elem.value = i; + if (i === config.paletteDefault) { + elem.selected = true; + } + + elem.appendChild( doc.createTextNode(lang.colorPalettes[i]) ); + _self.elems.cpaletteInput.appendChild(elem); + } + + // This is the ordered list where we add each color (list item). + _self.elems.cpaletteOutput = gui.elems.colormixer_cpaletteOutput; + _self.elems.cpaletteOutput.addEventListener('click', _self.ev_click_color, + false); + + _self.cpalette_load(config.paletteDefault); + + // Make sure the Canvas element scale is in sync with the application. + app.events.add('canvasSizeChange', _self.update_dimensions); + + _self.panelSelector.events.add('guiTabActivate', _self.ev_tabActivate); + + // Make sure the Color Mixer is properly closed when the floating panel is + // closed. + _self.panel.events.add('guiFloatingPanelStateChange', + _self.ev_panel_stateChange); + + return true; + }; + + /** + * This function calculates lots of values used by the other CIE Lab-related + * functions. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.init_lab = function () { + var cfg = config.lab; + if (!cfg) { + return false; + } + + // Chromaticity coordinates for the RGB primaries. + var x0_r = cfg.x_r, + y0_r = cfg.y_r, + x0_g = cfg.x_g, + y0_g = cfg.y_g, + x0_b = cfg.x_b, + y0_b = cfg.y_b, + + // The reference white point (xyY to XYZ). + w_x = cfg.ref_x / cfg.ref_y, + w_y = 1, + w_z = (1 - cfg.ref_x - cfg.ref_y) / cfg.ref_y; + + cfg.w_x = w_x; + cfg.w_y = w_y; + cfg.w_z = w_z; + + // Again, xyY to XYZ for each RGB primary. Y=1. + var x_r = x0_r / y0_r, + y_r = 1, + z_r = (1 - x0_r - y0_r) / y0_r, + x_g = x0_g / y0_g, + y_g = 1, + z_g = (1 - x0_g - y0_g) / y0_g, + x_b = x0_b / y0_b, + y_b = 1, + z_b = (1 - x0_b - y0_b) / y0_b, + m = [x_r, y_r, z_r, + x_g, y_g, z_g, + x_b, y_b, z_b], + m_i = _self.calc_m3inv(m), + s = _self.calc_m1x3([w_x, w_y, w_z], m_i); + + // The 3x3 matrix used by rgb2xyz(). + m = [s[0] * m[0], s[0] * m[1], s[0] * m[2], + s[1] * m[3], s[1] * m[4], s[1] * m[5], + s[2] * m[6], s[2] * m[7], s[2] * m[8]]; + + // The matrix inverse, used by xyz2rgb(); + cfg.m_i = _self.calc_m3inv(m); + cfg.m = m; + + // Now determine the min/max values for a and b. + + var xyz = _self.rgb2xyz([0, 1, 0]), // green gives the minimum value for a + lab = _self.xyz2lab(xyz), + values = config.inputValues; + values.cie_a[0] = lab[1]; + + xyz = _self.rgb2xyz([1, 0, 1]); // magenta gives the maximum value for a + lab = _self.xyz2lab(xyz); + values.cie_a[1] = lab[1]; + + xyz = _self.rgb2xyz([0, 0, 1]); // blue gives the minimum value for b + lab = _self.xyz2lab(xyz); + values.cie_b[0] = lab[2]; + + xyz = _self.rgb2xyz([1, 1, 0]); // yellow gives the maximum value for b + lab = _self.xyz2lab(xyz); + values.cie_b[1] = lab[2]; + + return true; + }; + + /** + * The guiTabActivate event handler for the tab panel which holds + * the color mixer and the color palettes. When switching back to the color + * mixer, this method updates the Canvas. + * + * @private + * @param {pwlib.appEvent.guiTabActivate} ev The application event object. + */ + this.ev_tabActivate = function (ev) { + if (ev.tabId === 'mixer' && _self.update_canvas_needed) { + _self.update_canvas(null, true); + } + }; + + /** + * The click event handler for the Accept button. This method + * dispatches the {@link pwlib.appEvent.configChange} application event for + * the configuration property associated to the target input, and hides the + * Color Mixer floating panel. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_click_accept = function (ev) { + ev.preventDefault(); + + var configProperty = _self.targetInput.configProperty, + configGroup = _self.targetInput.configGroup, + configGroupRef = _self.targetInput.configGroupRef, + prevVal = configGroupRef[configProperty], + newVal = 'rgba(' + MathRound(_self.color.red * 255) + ',' + + MathRound(_self.color.green * 255) + ',' + + MathRound(_self.color.blue * 255) + ',' + + _self.color.alpha + ')'; + + _self.hide(); + + if (prevVal !== newVal) { + configGroupRef[configProperty] = newVal; + app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, + configProperty, configGroup, configGroupRef)); + } + }; + + /** + * The click event handler for the Cancel button. This method + * hides the Color Mixer floating panel. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_click_cancel = function (ev) { + ev.preventDefault(); + _self.hide(); + }; + + /** + * The click event handler for the "Save color" button. This + * method adds the current color into the "_saved" color palette. + * + * @private + * @param {Event} ev The DOM Event object. + */ + // TODO: provide a way to save the color palette permanently. This should use + // some application event. + this.ev_click_saveColor = function (ev) { + ev.preventDefault(); + + var color = [_self.color.red, _self.color.green, _self.color.blue], + saved = config.colorPalettes._saved; + + saved.colors.push(color); + + _self.elems.cpaletteInput.value = '_saved'; + _self.cpalette_load('_saved'); + _self.panelSelector.tabActivate('cpalettes'); + + return true; + }; + + /** + * The click event handler for the "Pick color" button. This + * method activates the color picker tool. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_click_pickColor = function (ev) { + ev.preventDefault(); + app.toolActivate('cpicker', ev); + }; + + /** + * The change event handler for the color palette input element. + * This loads the color palette the user selected. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_change_cpalette = function (ev) { + _self.cpalette_load(this.value); + }; + + /** + * Load a color palette. Loading is performed asynchronously. + * + * @param {String} id The color palette ID. + * + * @returns {Boolean} True if the load was successful, or false if not. + */ + this.cpalette_load = function (id) { + if (!id || !(id in config.colorPalettes)) { + return false; + } + + var palette = config.colorPalettes[id]; + + if (palette.file) { + pwlib.xhrLoad(PaintWeb.baseFolder + palette.file, this.cpalette_loaded); + + return true; + + } else if (palette.colors) { + return this.cpalette_show(palette.colors); + + } else { + return false; + } + }; + + /** + * The onreadystatechange event handler for the color palette + * XMLHttpRequest object. + * + * @private + * @param {XMLHttpRequest} xhr The XMLHttpRequest object. + */ + this.cpalette_loaded = function (xhr) { + if (!xhr || xhr.readyState !== 4) { + return; + } + + if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { + alert(lang.failedColorPaletteLoad); + return; + } + + var colors = JSON.parse(xhr.responseText); + xhr = null; + _self.cpalette_show(colors); + }; + + /** + * Show a color palette. This method adds all the colors in the DOM as + * individual anchor elements which users can click on. + * + * @private + * + * @param {Array} colors The array which holds each color in the palette. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.cpalette_show = function (colors) { + if (!colors || !(colors instanceof Array)) { + return false; + } + + var color, anchor, rgbValue, + frag = doc.createDocumentFragment(), + dest = this.elems.cpaletteOutput; + + dest.style.display = 'none'; + while (dest.hasChildNodes()) { + dest.removeChild(dest.firstChild); + } + + for (var i = 0, n = colors.length; i < n; i++) { + color = colors[i]; + + // Do not allow values higher than 1. + color[0] = MathMin(1, color[0]); + color[1] = MathMin(1, color[1]); + color[2] = MathMin(1, color[2]); + + rgbValue = 'rgb(' + MathRound(color[0] * 255) + ',' + + MathRound(color[1] * 255) + ',' + + MathRound(color[2] * 255) + ')'; + + anchor = doc.createElement('a'); + anchor.href = '#'; + anchor._color = color; + anchor.style.backgroundColor = rgbValue; + anchor.appendChild(doc.createTextNode(rgbValue)); + + frag.appendChild(anchor); + } + + dest.appendChild(frag); + dest.style.display = 'block'; + + colors = frag = null; + + return true; + }; + + /** + * The click event handler for colors in the color palette list. + * This event handler is also used for the "old color" element. This method + * updates the color mixer to use the color the user picked. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_click_color = function (ev) { + var color = ev.target._color; + if (!color) { + return; + } + + ev.preventDefault(); + + _self.color.red = color[0]; + _self.color.green = color[1]; + _self.color.blue = color[2]; + + if (typeof(color[3]) !== 'undefined') { + _self.color.alpha = color[3]; + } + + _self.update_color('rgb'); + }; + + /** + * Recalculate the dimensions and coordinates for the slider and for the color + * space visualisation within the Canvas element. + * + *

This method is an event handler for the {@link + * pwlib.appEvent.canvasSizeChange} application event. + * + * @private + */ + this.update_dimensions = function () { + if (resScale === app.resolution.scale) { + return; + } + + resScale = app.resolution.scale; + + var canvas = _self.context2d.canvas, + width = canvas.width, + height = canvas.height, + sWidth = width / resScale, + sHeight = height / resScale, + style; + + _self.sliderWidth = MathRound(width * config.sliderWidth); + _self.sliderHeight = height - 1; + _self.sliderSpacing = MathRound(width * config.sliderSpacing); + _self.sliderX = width - _self.sliderWidth - 2; + _self.chartWidth = _self.sliderX - _self.sliderSpacing; + _self.chartHeight = height; + + style = _self.elems.controls.style; + style.width = sWidth + 'px'; + style.height = sHeight + 'px'; + + style = _self.elems.slider.style; + style.width = (_self.sliderWidth / resScale) + 'px'; + style.left = (_self.sliderX / resScale) + 'px'; + + style = canvas.style; + style.width = sWidth + 'px'; + style.height = sHeight + 'px'; + + if (_self.panel.state !== _self.panel.STATE_HIDDEN) { + _self.update_canvas(); + } + }; + + /** + * Calculate the product of two matrices. + * + *

Matrices are one-dimensional arrays of the form [a0, a1, a2, ..., + * b0, b1, b2, ...] where each element from the matrix is given in + * order, from the left to the right, row by row from the top to the bottom. + * + * @param {Array} a The first matrix must be one row and three columns. + * @param {Array} b The second matrix must be three rows and three columns. + * + * @returns {Array} The matrix product, one row and three columns. + */ + // Note: for obvious reasons, this method is not a full-fledged matrix product + // calculator. It's as simple as possible - fitting only the very specific + // needs of the color mixer. + this.calc_m1x3 = function (a, b) { + if (!(a instanceof Array) || !(b instanceof Array)) { + return false; + } else { + return [ + a[0] * b[0] + a[1] * b[3] + a[2] * b[6], // x + a[0] * b[1] + a[1] * b[4] + a[2] * b[7], // y + a[0] * b[2] + a[1] * b[5] + a[2] * b[8] // z + ]; + } + }; + + /** + * Calculate the matrix inverse. + * + *

Matrices are one-dimensional arrays of the form [a0, a1, a2, ..., + * b0, b1, b2, ...] where each element from the matrix is given in + * order, from the left to the right, row by row from the top to the bottom. + * + * @private + * + * @param {Array} m The square matrix which must have three rows and three + * columns. + * + * @returns {Array|false} The computed matrix inverse, or false if the matrix + * determinant was 0 - the given matrix is not invertible. + */ + // Note: for obvious reasons, this method is not a full-fledged matrix inverse + // calculator. It's as simple as possible - fitting only the very specific + // needs of the color mixer. + this.calc_m3inv = function (m) { + if (!(m instanceof Array)) { + return false; + } + + var d = (m[0]*m[4]*m[8] + m[1]*m[5]*m[6] + m[2]*m[3]*m[7]) - + (m[2]*m[4]*m[6] + m[5]*m[7]*m[0] + m[8]*m[1]*m[3]); + + // Matrix determinant is 0: the matrix is not invertible. + if (d === 0) { + return false; + } + + var i = [ + m[4]*m[8] - m[5]*m[7], -m[3]*m[8] + m[5]*m[6], m[3]*m[7] - m[4]*m[6], + -m[1]*m[8] + m[2]*m[7], m[0]*m[8] - m[2]*m[6], -m[0]*m[7] + m[1]*m[6], + m[1]*m[5] - m[2]*m[4], -m[0]*m[5] + m[2]*m[3], m[0]*m[4] - m[1]*m[3] + ]; + + i = [1/d * i[0], 1/d * i[3], 1/d * i[6], + 1/d * i[1], 1/d * i[4], 1/d * i[7], + 1/d * i[2], 1/d * i[5], 1/d * i[8]]; + + return i; + }; + + /** + * The change event handler for the Color Mixer inputs of + * type=radio. This method allows users to change the active color key - used + * for the color space visualisation. + * @private + */ + this.ev_change_ckey_active = function () { + if (this.value && this.value !== _self.ckey_active) { + _self.update_ckey_active(this.value); + } + }; + + /** + * Update the active color key. This method updates the Canvas accordingly. + * + * @private + * + * @param {String} ckey The color key you want to be active. + * @param {Boolean} [only_vars] Tells if you want only the variables to be + * updated - no Canvas updates. This is true only during the Color Mixer + * initialization. + * + * @return {Boolean} True if the operation was successful, or false if not. + */ + this.update_ckey_active = function (ckey, only_vars) { + if (!_self.inputs[ckey]) { + return false; + } + + _self.ckey_active = ckey; + + var adjoint = [], group = _self.ckey_grouping[ckey]; + + // Determine the adjoint color keys. For example, if red is active, then adjoint = ['green', 'blue']. + for (var i in _self.ckey_grouping) { + if (_self.ckey_grouping[i] === group && i !== ckey) { + adjoint.push(i); + } + } + + _self.ckey_active_group = group; + _self.ckey_adjoint = adjoint; + + if (!only_vars) { + if (_self.panelSelector.tabId !== 'mixer') { + _self.update_canvas_needed = true; + _self.panelSelector.tabActivate('mixer'); + } else { + _self.update_canvas(); + } + + if (_self.panelInputs.tabId !== group) { + _self.panelInputs.tabActivate(group); + } + } + + return true; + }; + + /** + * Show the Color Mixer. + * + * @param {Object} target The target input object. + * + * @param {Object} color The color you want to set before the Color Mixer is + * shown. The object must have four properties: red, + * green, blue and alpha. All the values must + * be between 0 and 1. This color becomes the "active color" and the "old + * color". + * + * @see this.targetInput for more information about the target + * object. + */ + this.show = function (target, color) { + var styleActive = _self.elems.colorActive.style, + colorOld = _self.elems.colorOld, + styleOld = colorOld.style; + + if (target) { + if (_self.targetInput) { + _self.targetInput.hide(); + } + + _self.targetInput = target; + _self.targetInput.show(); + } + + if (color) { + _self.color.red = color.red; + _self.color.green = color.green; + _self.color.blue = color.blue; + _self.color.alpha = color.alpha; + + _self.update_color('rgb'); + + styleOld.backgroundColor = styleActive.backgroundColor; + styleOld.opacity = styleActive.opacity; + colorOld._color = [color.red, color.green, color.blue, color.alpha]; + } + + _self.panel.show(); + }; + + /** + * Hide the Color Mixer floating panel. This method invokes the + * hide() method provided by the target input. + */ + this.hide = function () { + _self.panel.hide(); + _self.ev_canvas_mode = false; + }; + + /** + * The guiFloatingPanelStateChange event handler for the Color + * Mixer panel. This method ensures the Color Mixer is properly closed. + * + * @param {pwlib.appEvent.guiFloatingPanelStateChange} ev The application + * event object. + */ + this.ev_panel_stateChange = function (ev) { + if (ev.state === ev.STATE_HIDDEN) { + if (_self.targetInput) { + _self.targetInput.hide(); + _self.targetInput = null; + } + _self.ev_canvas_mode = false; + } + }; + + /** + * The input and change event handler for all the + * Color Mixer inputs. + * @private + */ + this.ev_input_change = function () { + if (!this._ckey) { + return; + } + + // Validate and restrict the possible values. + // If the input is unchanged, or if the new value is invalid, the function + // stops. + // The hexadecimal input is checked with a simple regular expression. + + if ((this._ckey === 'hex' && !/^\#[a-f0-9]{6}$/i.test(this.value))) { + return; + } + + if (this.getAttribute('type') === 'number') { + var val = parseInt(this.value), + min = this.getAttribute('min'), + max = this.getAttribute('max'); + + if (isNaN(val)) { + val = min; + } + + if (val < min) { + val = min; + } else if (val > max) { + val = max; + } + + if (val != this.value) { + this.value = val; + } + } + + // Update the internal color value. + if (this._ckey === 'hex') { + _self.color[this._ckey] = this.value; + } else if (_self.ckey_grouping[this._ckey] === 'lab') { + _self.color[this._ckey] = parseInt(this.value); + } else { + _self.color[this._ckey] = parseInt(this.value) + / config.inputValues[this._ckey][1]; + } + + _self.update_color(this._ckey); + }; + + /** + * Update the current color. Once a color value is updated, this method is + * called to keep the rest of the color mixer in sync: for example, when a RGB + * value is updated, it needs to be converted to HSV, CMYK and all of the + * other formats. Additionally, this method updates the color preview, the + * controls on the Canvas and the input values. + * + *

You need to call this function whenever you update the color manually. + * + * @param {String} ckey The color key that was updated. + */ + this.update_color = function (ckey) { + var group = _self.ckey_grouping[ckey] || ckey; + + switch (group) { + case 'rgb': + _self.rgb2hsv(); + _self.rgb2hex(); + _self.rgb2lab(); + _self.rgb2cmyk(); + break; + + case 'hsv': + _self.hsv2rgb(); + _self.rgb2hex(); + _self.rgb2lab(); + _self.rgb2cmyk(); + break; + + case 'hex': + _self.hex2rgb(); + _self.rgb2hsv(); + _self.rgb2lab(); + _self.rgb2cmyk(); + break; + + case 'lab': + _self.lab2rgb(); + _self.rgb2hsv(); + _self.rgb2hex(); + _self.rgb2cmyk(); + break; + + case 'cmyk': + _self.cmyk2rgb(); + _self.rgb2lab(); + _self.rgb2hsv(); + _self.rgb2hex(); + } + + _self.update_preview(); + _self.update_inputs(); + + if (ckey !== 'alpha') { + _self.update_canvas(ckey); + } + }; + + /** + * Update the color preview. + * @private + */ + this.update_preview = function () { + var red = MathRound(_self.color.red * 255), + green = MathRound(_self.color.green * 255), + blue = MathRound(_self.color.blue * 255), + style = _self.elems.colorActive.style; + + style.backgroundColor = 'rgb(' + red + ',' + green + ',' + blue + ')'; + style.opacity = _self.color.alpha; + }; + + /** + * Update the color inputs. This method takes the internal color values and + * shows them in the DOM input elements. + * @private + */ + this.update_inputs = function () { + var input; + for (var i in _self.inputs) { + input = _self.inputs[i]; + input._old_value = input.value; + if (input._ckey === 'hex') { + input.value = _self.color[i]; + } else if (_self.ckey_grouping[input._ckey] === 'lab') { + input.value = MathRound(_self.color[i]); + } else { + input.value = MathRound(_self.color[i] * config.inputValues[i][1]); + } + } + }; + + /** + * Convert RGB to CMYK. This uses the current color RGB values and updates the + * CMYK values accordingly. + * @private + */ + // Quote from Wikipedia: + // "Since RGB and CMYK spaces are both device-dependent spaces, there is no + // simple or general conversion formula that converts between them. + // Conversions are generally done through color management systems, using + // color profiles that describe the spaces being converted. Nevertheless, the + // conversions cannot be exact, since these spaces have very different + // gamuts." + // Translation: this is just a simple RGB to CMYK conversion function. + this.rgb2cmyk = function () { + var color = _self.color, + cyan, magenta, yellow, black, + red = color.red, + green = color.green, + blue = color.blue; + + cyan = 1 - red; + magenta = 1 - green; + yellow = 1 - blue; + + black = MathMin(cyan, magenta, yellow, 1); + + if (black === 1) { + cyan = magenta = yellow = 0; + } else { + var w = 1 - black; + cyan = (cyan - black) / w; + magenta = (magenta - black) / w; + yellow = (yellow - black) / w; + } + + color.cyan = cyan; + color.magenta = magenta; + color.yellow = yellow; + color.black = black; + }; + + /** + * Convert CMYK to RGB (internally). + * @private + */ + this.cmyk2rgb = function () { + var color = _self.color, + w = 1 - color.black; + + color.red = 1 - color.cyan * w - color.black; + color.green = 1 - color.magenta * w - color.black; + color.blue = 1 - color.yellow * w - color.black; + }; + + /** + * Convert RGB to HSV (internally). + * @private + */ + this.rgb2hsv = function () { + var hue, sat, val, // HSV + red = _self.color.red, + green = _self.color.green, + blue = _self.color.blue, + min = MathMin(red, green, blue), + max = MathMax(red, green, blue), + delta = max - min, + val = max; + + // This is gray (red==green==blue) + if (delta === 0) { + hue = sat = 0; + } else { + sat = delta / max; + + if (max === red) { + hue = (green - blue) / delta; + } else if (max === green) { + hue = (blue - red) / delta + 2; + } else if (max === blue) { + hue = (red - green) / delta + 4; + } + + hue /= 6; + if (hue < 0) { + hue += 1; + } + } + + _self.color.hue = hue; + _self.color.sat = sat; + _self.color.val = val; + }; + + /** + * Convert HSV to RGB. + * + * @private + * + * @param {Boolean} [no_update] Tells the function to not update the internal + * RGB color values. + * @param {Array} [hsv] The array holding the HSV values you want to convert + * to RGB. This array must have three elements ordered as: hue, + * saturation and value - all between 0 and 1. If you do + * not provide the array, then the internal HSV color values are used. + * + * @returns {Array} The RGB values converted from HSV. The array has three + * elements ordered as: red, green and blue + * - all with values between 0 and 1. + */ + this.hsv2rgb = function (no_update, hsv) { + var color = _self.color, + red, green, blue, hue, sat, val; + + // Use custom HSV values or the current color. + if (hsv) { + hue = hsv[0]; + sat = hsv[1]; + val = hsv[2]; + } else { + hue = color.hue, + sat = color.sat, + val = color.val; + } + + // achromatic (grey) + if (sat === 0) { + red = green = blue = val; + } else { + var h = hue * 6; + var i = MathFloor(h); + var t1 = val * ( 1 - sat ), + t2 = val * ( 1 - sat * ( h - i ) ), + t3 = val * ( 1 - sat * ( 1 - (h - i) ) ); + + if (i === 0 || i === 6) { // 0° Red + red = val; green = t3; blue = t1; + } else if (i === 1) { // 60° Yellow + red = t2; green = val; blue = t1; + } else if (i === 2) { // 120° Green + red = t1; green = val; blue = t3; + } else if (i === 3) { // 180° Cyan + red = t1; green = t2; blue = val; + } else if (i === 4) { // 240° Blue + red = t3; green = t1; blue = val; + } else if (i === 5) { // 300° Magenta + red = val; green = t1; blue = t2; + } + } + + if (!no_update) { + color.red = red; + color.green = green; + color.blue = blue; + } + + return [red, green, blue]; + }; + + /** + * Convert RGB to hexadecimal representation (internally). + * @private + */ + this.rgb2hex = function () { + var hex = '#', rgb = ['red', 'green', 'blue'], i, val, + color = _self.color; + + for (i = 0; i < 3; i++) { + val = MathRound(color[rgb[i]] * 255).toString(16); + if (val.length === 1) { + val = '0' + val; + } + hex += val; + } + + color.hex = hex; + }; + + /** + * Convert the hexadecimal representation of color to RGB values (internally). + * @private + */ + this.hex2rgb = function () { + var rgb = ['red', 'green', 'blue'], i, val, + color = _self.color, + hex = color.hex; + + hex = hex.substr(1); + if (hex.length !== 6) { + return; + } + + for (i = 0; i < 3; i++) { + val = hex.substr(i*2, 2); + color[rgb[i]] = parseInt(val, 16)/255; + } + }; + + /** + * Convert RGB to CIE Lab (internally). + * @private + */ + this.rgb2lab = function () { + var color = _self.color, + lab = _self.xyz2lab(_self.rgb2xyz([color.red, color.green, + color.blue])); + + color.cie_l = lab[0]; + color.cie_a = lab[1]; + color.cie_b = lab[2]; + }; + + /** + * Convert CIE Lab values to RGB values (internally). + * @private + */ + this.lab2rgb = function () { + var color = _self.color, + rgb = _self.xyz2rgb(_self.lab2xyz(color.cie_l, color.cie_a, + color.cie_b)); + + color.red = rgb[0]; + color.green = rgb[1]; + color.blue = rgb[2]; + }; + + /** + * Convert XYZ color values into CIE Lab values. + * + * @private + * + * @param {Array} xyz The array holding the XYZ color values in order: + * X, Y and Z. + * + * @returns {Array} An array holding the CIE Lab values in order: + * L, a and b. + */ + this.xyz2lab = function (xyz) { + var cfg = config.lab, + + // 216/24389 or (6/29)^3 (both = 0.008856...) + e = 216/24389, + + // 903.296296... + k = 24389/27; + + xyz[0] /= cfg.w_x; + xyz[1] /= cfg.w_y; + xyz[2] /= cfg.w_z; + + if (xyz[0] > e) { + xyz[0] = MathPow(xyz[0], 1/3); + } else { + xyz[0] = (k*xyz[0] + 16)/116; + } + + if (xyz[1] > e) { + xyz[1] = MathPow(xyz[1], 1/3); + } else { + xyz[1] = (k*xyz[1] + 16)/116; + } + + if (xyz[2] > e) { + xyz[2] = MathPow(xyz[2], 1/3); + } else { + xyz[2] = (k*xyz[2] + 16)/116; + } + + var cie_l = 116 * xyz[1] - 16, + cie_a = 500 * (xyz[0] - xyz[1]), + cie_b = 200 * (xyz[1] - xyz[2]); + + return [cie_l, cie_a, cie_b]; + }; + + /** + * Convert CIE Lab values to XYZ color values. + * + * @private + * + * @param {Number} cie_l The color lightness value. + * @param {Number} cie_a The a* color opponent. + * @param {Number} cie_b The b* color opponent. + * + * @returns {Array} An array holding the XYZ color values in order: + * X, Y and Z. + */ + this.lab2xyz = function (cie_l, cie_a, cie_b) { + var y = (cie_l + 16) / 116, + x = y + cie_a / 500, + z = y - cie_b / 200, + + // 0.206896551... + e = 6/29, + + // 7.787037... + k = 1/3 * MathPow(29/6, 2), + + // 0.137931... + t = 16/116, + cfg = config.lab; + + if (x > e) { + x = MathPow(x, 3); + } else { + x = (x - t) / k; + } + + if (y > e) { + y = MathPow(y, 3); + } else { + y = (y - t) / k; + } + + if (z > e) { + z = MathPow(z, 3); + } else { + z = (z - t) / k; + } + + x *= cfg.w_x; + y *= cfg.w_y; + z *= cfg.w_z; + + return [x, y, z]; + }; + + /** + * Convert XYZ color values to RGB. + * + * @private + * + * @param {Array} xyz The array holding the XYZ color values in order: + * X, Y and Z + * + * @returns {Array} An array holding the RGB values in order: red, + * green and blue. + */ + this.xyz2rgb = function (xyz) { + var rgb = _self.calc_m1x3(xyz, config.lab.m_i); + + if (rgb[0] > 0.0031308) { + rgb[0] = 1.055 * MathPow(rgb[0], 1 / 2.4) - 0.055; + } else { + rgb[0] *= 12.9232; + } + + if (rgb[1] > 0.0031308) { + rgb[1] = 1.055 * MathPow(rgb[1], 1 / 2.4) - 0.055; + } else { + rgb[1] *= 12.9232; + } + + if (rgb[2] > 0.0031308) { + rgb[2] = 1.055 * MathPow(rgb[2], 1 / 2.4) - 0.055; + } else { + rgb[2] *= 12.9232; + } + + if (rgb[0] < 0) { + rgb[0] = 0; + } else if (rgb[0] > 1) { + rgb[0] = 1; + } + + if (rgb[1] < 0) { + rgb[1] = 0; + } else if (rgb[1] > 1) { + rgb[1] = 1; + } + + if (rgb[2] < 0) { + rgb[2] = 0; + } else if (rgb[2] > 1) { + rgb[2] = 1; + } + + return rgb; + }; + + /** + * Convert RGB values to XYZ color values. + * + * @private + * + * @param {Array} rgb The array holding the RGB values in order: + * red, green and blue. + * + * @returns {Array} An array holding the XYZ color values in order: + * X, Y and Z. + */ + this.rgb2xyz = function (rgb) { + if (rgb[0] > 0.04045) { + rgb[0] = MathPow(( rgb[0] + 0.055 ) / 1.055, 2.4); + } else { + rgb[0] /= 12.9232; + } + + if (rgb[1] > 0.04045) { + rgb[1] = MathPow(( rgb[1] + 0.055 ) / 1.055, 2.4); + } else { + rgb[1] /= 12.9232; + } + + if (rgb[2] > 0.04045) { + rgb[2] = MathPow(( rgb[2] + 0.055 ) / 1.055, 2.4); + } else { + rgb[2] /= 12.9232; + } + + return _self.calc_m1x3(rgb, config.lab.m); + }; + + /** + * Update the color space visualisation. This method updates the color chart + * and/or the color slider, and the associated controls, each as needed when + * a color key is updated. + * + * @private + * + * @param {String} updated_ckey The color key that was updated. + * @param {Boolean} [force=false] Tells the function to force an update. The + * Canvas is not updated when the color mixer panel is not visible. + * + * @returns {Boolean} If the operation was successful, or false if not. + */ + this.update_canvas = function (updated_ckey, force) { + if (_self.panelSelector.tabId !== 'mixer' && !force) { + _self.update_canvas_needed = true; + return true; + } + + _self.update_canvas_needed = false; + + var slider = _self.elems.slider.style, + chart = _self.elems.chartDot.style, + color = _self.color, + ckey = _self.ckey_active, + group = _self.ckey_active_group, + adjoint = _self.ckey_adjoint, + width = _self.chartWidth / resScale, + height = _self.chartHeight / resScale, + mx, my, sy; + + // Update the slider which shows the position of the active ckey. + if (updated_ckey !== adjoint[0] && updated_ckey !== adjoint[1] && + _self.ev_canvas_mode !== 'chart') { + if (group === 'lab') { + sy = (color[ckey] - config.inputValues[ckey][0]) / _self.abs_max[ckey]; + } else { + sy = color[ckey]; + } + + if (ckey !== 'hue' && group !== 'lab') { + sy = 1 - sy; + } + + slider.top = MathRound(sy * height) + 'px'; + } + + // Update the chart dot. + if (updated_ckey !== ckey) { + if (group === 'lab') { + mx = (color[adjoint[0]] - config.inputValues[adjoint[0]][0]) + / _self.abs_max[adjoint[0]]; + my = (color[adjoint[1]] - config.inputValues[adjoint[1]][0]) + / _self.abs_max[adjoint[1]]; + } else { + mx = color[adjoint[0]]; + my = 1 - color[adjoint[1]]; + } + + chart.top = MathRound(my * height) + 'px'; + chart.left = MathRound(mx * width) + 'px'; + } + + if (!_self.draw_chart(updated_ckey) || !_self.draw_slider(updated_ckey)) { + return false; + } else { + return true; + } + }; + + /** + * The mouse events handler for the Canvas controls. This method determines + * the region the user is using, and it also updates the color values for the + * active color key. The Canvas and all the inputs in the color mixer are + * updated as needed. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.ev_canvas = function (ev) { + ev.preventDefault(); + + // Initialize color picking only on mousedown. + if (ev.type === 'mousedown' && !_self.ev_canvas_mode) { + _self.ev_canvas_mode = true; + doc.addEventListener('mouseup', _self.ev_canvas, false); + } + + if (!_self.ev_canvas_mode) { + return false; + } + + // The mouseup event stops the effect of any further mousemove events. + if (ev.type === 'mouseup') { + _self.ev_canvas_mode = false; + doc.removeEventListener('mouseup', _self.ev_canvas, false); + } + + var elems = _self.elems; + + // If the user is on top of the 'controls' element, determine the mouse coordinates and the 'mode' for this function: the user is either working with the slider, or he/she is working with the color chart itself. + if (ev.target === elems.controls) { + var mx, my, + width = _self.context2d.canvas.width, + height = _self.context2d.canvas.height; + + // Get the mouse position, relative to the event target. + if (ev.layerX || ev.layerX === 0) { // Firefox + mx = ev.layerX * resScale; + my = ev.layerY * resScale; + } else if (ev.offsetX || ev.offsetX === 0) { // Opera + mx = ev.offsetX * resScale; + my = ev.offsetY * resScale; + } + + if (mx >= 0 && mx <= _self.chartWidth) { + mode = 'chart'; + } else if (mx >= _self.sliderX && mx <= width) { + mode = 'slider'; + } + } else { + // The user might have clicked on the chart dot, or on the slider graphic + // itself. + // If yes, then determine the mode based on this. + if (ev.target === elems.chartDot) { + mode = 'chart'; + } else if (ev.target === elems.slider) { + mode = 'slider'; + } + } + + // Update the ev_canvas_mode value to include the mode name, if it's simply + // the true boolean. + // This ensures that the continuous mouse movements do not go from one mode + // to another when the user moves out from the slider to the chart (and + // vice-versa). + if (mode && _self.ev_canvas_mode === true) { + _self.ev_canvas_mode = mode; + } + + // Do not continue if the mode wasn't determined (the mouse is not on the + // slider, nor on the chart). + // Also don't continue if the mouse is not in the same place (different + // mode). + if (!mode || _self.ev_canvas_mode !== mode || ev.target !== elems.controls) + { + return false; + } + + var color = _self.color, + val_x = mx / _self.chartWidth, + val_y = my / height; + + if (mode === 'slider') { + if (_self.ckey_active === 'hue') { + color[_self.ckey_active] = val_y; + } else if (_self.ckey_active_group === 'lab') { + color[_self.ckey_active] = _self.abs_max[_self.ckey_active] * val_y + + config.inputValues[_self.ckey_active][0]; + } else { + color[_self.ckey_active] = 1 - val_y; + } + + return _self.update_color(_self.ckey_active); + + } else if (mode === 'chart') { + if (val_x > 1) { + return false; + } + + if (_self.ckey_active_group === 'lab') { + val_x = _self.abs_max[_self.ckey_adjoint[0]] * val_x + + config.inputValues[_self.ckey_adjoint[0]][0]; + val_y = _self.abs_max[_self.ckey_adjoint[1]] * val_y + + config.inputValues[_self.ckey_adjoint[1]][0]; + } else { + val_y = 1 - val_y; + } + + color[_self.ckey_adjoint[0]] = val_x; + color[_self.ckey_adjoint[1]] = val_y; + + return _self.update_color(_self.ckey_active_group); + } + + return false; + }; + + /** + * Draw the color space visualisation. + * + * @private + * + * @param {String} updated_ckey The color key that was updated. This is used + * to determine if the Canvas needs to be updated or not. + */ + this.draw_chart = function (updated_ckey) { + var context = _self.context2d, + gradient, color, opacity, i; + + if (updated_ckey === _self.ckey_adjoint[0] || updated_ckey === + _self.ckey_adjoint[1] || (_self.ev_canvas_mode === 'chart' && + updated_ckey === _self.ckey_active_group)) { + return true; + } + + var w = _self.chartWidth, + h = _self.chartHeight; + + context.clearRect(0, 0, w, h); + + if (_self.ckey_active === 'sat') { + // In saturation mode the user has the slider which allows him/her to + // change the saturation (hSv) of the current color. + // The chart shows the hue spectrum on the X axis, while the Y axis gives + // the Value (hsV). + + if (_self.color.sat > 0) { + // Draw the hue spectrum gradient on the X axis. + gradient = context.createLinearGradient(0, 0, w, 0); + for (i = 0; i <= 6; i++) { + color = 'rgb(' + hueSpectrum[i][0] + ', ' + + hueSpectrum[i][1] + ', ' + + hueSpectrum[i][2] + ')'; + gradient.addColorStop(i * 1/6, color); + } + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + // Draw the gradient which darkens the hue spectrum on the Y axis. + gradient = context.createLinearGradient(0, 0, 0, h); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, 'rgba(0, 0, 0, 1)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + } + + if (_self.color.sat < 1) { + // Draw the white to black gradient. This is used for creating the + // saturation effect. Lowering the saturation value makes the gradient + // more visible, hence the hue colors desaturate. + opacity = 1 - _self.color.sat; + gradient = context.createLinearGradient(0, 0, 0, h); + gradient.addColorStop(0, 'rgba(255, 255, 255, ' + opacity + ')'); + gradient.addColorStop(1, 'rgba( 0, 0, 0, ' + opacity + ')'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + } + + } else if (_self.ckey_active === 'val') { + // In value mode the user has the slider which allows him/her to change the value (hsV) of the current color. + // The chart shows the hue spectrum on the X axis, while the Y axis gives the saturation (hSv). + + if (_self.color.val > 0) { + // Draw the hue spectrum gradient on the X axis. + gradient = context.createLinearGradient(0, 0, w, 0); + for (i = 0; i <= 6; i++) { + color = 'rgb(' + hueSpectrum[i][0] + ', ' + + hueSpectrum[i][1] + ', ' + + hueSpectrum[i][2] + ')'; + gradient.addColorStop(i * 1/6, color); + } + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + // Draw the gradient which lightens the hue spectrum on the Y axis. + gradient = context.createLinearGradient(0, 0, 0, h); + gradient.addColorStop(0, 'rgba(255, 255, 255, 0)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 1)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + } + + if (_self.color.val < 1) { + // Draw a solid black color on top. This is used for darkening the hue colors gradient when the user reduces the Value (hsV). + context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) +')'; + context.fillRect(0, 0, w, h); + } + + } else if (_self.ckey_active === 'hue') { + // In hue mode the user has the slider which allows him/her to change the hue (Hsv) of the current color. + // The chart shows the current color in the background. The X axis gives the saturation (hSv), and the Y axis gives the value (hsV). + + if (_self.color.sat === 1 && _self.color.val === 1) { + color = [_self.color.red, _self.color.green, _self.color.blue]; + } else { + // Determine the RGB values for the current color which has the same hue, but maximum saturation and value (hSV). + color = _self.hsv2rgb(true, [_self.color.hue, 1, 1]); + } + for (i = 0; i < 3; i++) { + color[i] = MathRound(color[i] * 255); + } + + context.fillStyle = 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')'; + context.fillRect(0, 0, w, h); + + // Draw the white gradient for saturation (X axis, hSv). + gradient = context.createLinearGradient(0, 0, w, 0); + gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + // Draw the black gradient for value (Y axis, hsV). + gradient = context.createLinearGradient(0, 0, 0, h); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, 'rgba(0, 0, 0, 1)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + } else if (_self.ckey_active_group === 'rgb') { + // In any red/green/blue mode the background color becomes the one of the ckey_active. Say, for ckey_active=red the background color would be the current red value (green and blue are both set to 0). + // On the X/Y axes the other two colors are shown. E.g. for red the X axis gives the green gradient, and the Y axis gives the blue gradient. The two gradients are drawn on top of the red background using a global composite operation (lighter) - to create the color addition effect. + var color2, color3; + + color = {'red' : 0, 'green' : 0, 'blue' : 0}; + color[_self.ckey_active] = MathRound(_self.color[_self.ckey_active] + * 255); + + color2 = {'red' : 0, 'green' : 0, 'blue' : 0}; + color2[_self.ckey_adjoint[1]] = 255; + + color3 = {'red' : 0, 'green' : 0, 'blue' : 0}; + color3[_self.ckey_adjoint[0]] = 255; + + // The background. + context.fillStyle = 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')'; + context.fillRect(0, 0, w, h); + + // This doesn't work in Opera 9.2 and older versions. + var op = context.globalCompositeOperation; + context.globalCompositeOperation = 'lighter'; + + // The Y axis gradient. + gradient = context.createLinearGradient(0, 0, 0, h); + gradient.addColorStop(0, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 1)'); + gradient.addColorStop(1, 'rgba(' + color2.red + ',' + color2.green + ',' + color2.blue + ', 0)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + // The X axis gradient. + gradient = context.createLinearGradient(0, 0, w, 0); + gradient.addColorStop(0, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 0)'); + gradient.addColorStop(1, 'rgba(' + color3.red + ',' + color3.green + ',' + color3.blue + ', 1)'); + context.fillStyle = gradient; + context.fillRect(0, 0, w, h); + + context.globalCompositeOperation = op; + + } else if (_self.ckey_active_group === 'lab') { + // The chart plots the CIE Lab colors. The non-active color keys give the X/Y axes. For example, if cie_l (lightness) is active, then the cie_a values give the X axis, and the Y axis is given by the values of cie_b. + // The chart is drawn manually, pixel-by-pixel, due to the special way CIE Lab works. This is very slow in today's UAs. + + var imgd = false; + + if (context.createImageData) { + imgd = context.createImageData(w, h); + } else if (context.getImageData) { + imgd = context.getImageData(0, 0, w, h); + } else { + imgd = { + 'width' : w, + 'height' : h, + 'data' : new Array(w*h*4) + }; + } + + var pix = imgd.data, + n = imgd.data.length - 1, + i = -1, p = 0, inc_x, inc_y, xyz = [], rgb = [], cie_x, cie_y; + + cie_x = _self.ckey_adjoint[0]; + cie_y = _self.ckey_adjoint[1]; + + color = { + 'cie_l' : _self.color.cie_l, + 'cie_a' : _self.color.cie_a, + 'cie_b' : _self.color.cie_b + }; + + inc_x = _self.abs_max[cie_x] / w; + inc_y = _self.abs_max[cie_y] / h; + + color[cie_x] = config.inputValues[cie_x][0]; + color[cie_y] = config.inputValues[cie_y][0]; + + while (i < n) { + xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b); + rgb = _self.xyz2rgb(xyz); + + pix[++i] = MathRound(rgb[0]*255); + pix[++i] = MathRound(rgb[1]*255); + pix[++i] = MathRound(rgb[2]*255); + pix[++i] = 255; + + p++; + color[cie_x] += inc_x; + + if ((p % w) === 0) { + color[cie_x] = config.inputValues[cie_x][0]; + color[cie_y] += inc_y; + } + } + + context.putImageData(imgd, 0, 0); + } + + return true; + }; + + /** + * Draw the color slider on the Canvas element. + * + * @private + * + * @param {String} updated_ckey The color key that was updated. This is used + * to determine if the Canvas needs to be updated or not. + */ + this.draw_slider = function (updated_ckey) { + if (_self.ckey_active === updated_ckey) { + return true; + } + + var context = _self.context2d, + slider_w = _self.sliderWidth, + slider_h = _self.sliderHeight, + slider_x = _self.sliderX, + slider_y = 0, + gradient, color, i; + + gradient = context.createLinearGradient(slider_x, slider_y, slider_x, slider_h); + + if (_self.ckey_active === 'hue') { + // Draw the hue spectrum gradient. + for (i = 0; i <= 6; i++) { + color = 'rgb(' + hueSpectrum[i][0] + ', ' + + hueSpectrum[i][1] + ', ' + + hueSpectrum[i][2] + ')'; + gradient.addColorStop(i * 1/6, color); + } + context.fillStyle = gradient; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + + if (_self.color.sat < 1) { + context.fillStyle = 'rgba(255, 255, 255, ' + + (1 - _self.color.sat) + ')'; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + } + if (_self.color.val < 1) { + context.fillStyle = 'rgba(0, 0, 0, ' + (1 - _self.color.val) + ')'; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + } + + } else if (_self.ckey_active === 'sat') { + // Draw the saturation gradient for the slider. + // The start color is the current color with maximum saturation. The bottom gradient color is the same "color" without saturation. + // The slider allows you to desaturate the current color. + + // Determine the RGB values for the current color which has the same hue and value (HsV), but maximum saturation (hSv). + if (_self.color.sat === 1) { + color = [_self.color.red, _self.color.green, _self.color.blue]; + } else { + color = _self.hsv2rgb(true, [_self.color.hue, 1, _self.color.val]); + } + + for (i = 0; i < 3; i++) { + color[i] = MathRound(color[i] * 255); + } + + var gray = MathRound(_self.color.val * 255); + gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')'); + gradient.addColorStop(1, 'rgb(' + gray + ', ' + gray + ', ' + gray + ')'); + context.fillStyle = gradient; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + + } else if (_self.ckey_active === 'val') { + // Determine the RGB values for the current color which has the same hue and saturation, but maximum value (hsV). + if (_self.color.val === 1) { + color = [_self.color.red, _self.color.green, _self.color.blue]; + } else { + color = _self.hsv2rgb(true, [_self.color.hue, _self.color.sat, 1]); + } + + for (i = 0; i < 3; i++) { + color[i] = MathRound(color[i] * 255); + } + + gradient.addColorStop(0, 'rgb(' + color[0] + ', ' + color[1] + ', ' + color[2] + ')'); + gradient.addColorStop(1, 'rgb(0, 0, 0)'); + context.fillStyle = gradient; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + + } else if (_self.ckey_active_group === 'rgb') { + var red = MathRound(_self.color.red * 255), + green = MathRound(_self.color.green * 255), + blue = MathRound(_self.color.blue * 255); + + color = { + 'red' : red, + 'green' : green, + 'blue' : blue + }; + color[_self.ckey_active] = 255; + + var color2 = { + 'red' : red, + 'green' : green, + 'blue' : blue + }; + color2[_self.ckey_active] = 0; + + gradient.addColorStop(0, 'rgb(' + color.red + ',' + color.green + ',' + color.blue + ')'); + gradient.addColorStop(1, 'rgb(' + color2.red + ',' + color2.green + ',' + color2.blue + ')'); + context.fillStyle = gradient; + context.fillRect(slider_x, slider_y, slider_w, slider_h); + + } else if (_self.ckey_active_group === 'lab') { + // The slider shows a gradient with the current color key going from the minimum to the maximum value. The gradient is calculated pixel by pixel, due to the special way CIE Lab is defined. + + var imgd = false; + + if (context.createImageData) { + imgd = context.createImageData(1, slider_h); + } else if (context.getImageData) { + imgd = context.getImageData(0, 0, 1, slider_h); + } else { + imgd = { + 'width' : 1, + 'height' : slider_h, + 'data' : new Array(slider_h*4) + }; + } + + var pix = imgd.data, + n = imgd.data.length - 1, + ckey = _self.ckey_active, + i = -1, inc, xyz, rgb; + + color = { + 'cie_l' : _self.color.cie_l, + 'cie_a' : _self.color.cie_a, + 'cie_b' : _self.color.cie_b + }; + + color[ckey] = config.inputValues[ckey][0]; + inc = _self.abs_max[ckey] / slider_h; + + while (i < n) { + xyz = _self.lab2xyz(color.cie_l, color.cie_a, color.cie_b); + rgb = _self.xyz2rgb(xyz); + pix[++i] = MathRound(rgb[0]*255); + pix[++i] = MathRound(rgb[1]*255); + pix[++i] = MathRound(rgb[2]*255); + pix[++i] = 255; + + color[ckey] += inc; + } + + for (i = 0; i <= slider_w; i++) { + context.putImageData(imgd, slider_x+i, slider_y); + } + } + + context.strokeStyle = '#6d6d6d'; + context.strokeRect(slider_x, slider_y, slider_w, slider_h); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/moodle.js b/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/moodle.js new file mode 100644 index 00000000..e9d3b71b --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/moodle.js @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-10-29 19:05:49 +0200 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the integration code for PaintWeb inside Moodle. + */ + +/** + * @class The Moodle extension for PaintWeb. This extension handles the Moodle + * integration inside the PaintWeb code. + * + *

Note: This extension is supposed to work with Moodle 1.9 + * and Moodle 2.0. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.extensions.moodle = function (app) { + var _self = this, + appEvent = pwlib.appEvent, + config = app.config, + gui = app.gui, + lang = app.lang.moodle, + moodleServer = config.moodleServer, + tinymceEditor = null, + qfErrorShown = false; + + // Holds information related to Moodle. + var moodleInfo = { + // Holds the URL of the image the user is saving. + imageURL: null, + + // The class name for the element which holds the textarea buttons (toggle + // on/off). + // This element exists only in Moodle 1.9. + textareaButtons: 'textareaicons', + + // The image save handler script on the server-side. The path is relative to + // the PaintWeb base folder. + // This used with Moodle 2.0. + imageSaveHandler20: '../ext/moodle/imagesave20.php', + + // The image save handler script for Moodle 1.9. + imageSaveHandler19: '../ext/moodle/imagesave19.php', + + // This holds the release version of Moodle being used. This should be 1.9 + // or 2.0. + release: 0, + + // Moodle 2.0 draft item ID used for file storage. + draftitemid: null + }; + + /** + * The extensionRegister event handler. Setup event listeners, + * determine Moodle version, and more. + * + * @returns {Boolean} True if the extension initialized successfully, or false + * if not. + */ + this.extensionRegister = function () { + // Register application events. + app.events.add('guiShow', this.guiShow); + app.events.add('guiHide', this.guiHide); + app.events.add('imageSave', this.imageSave); + + if (moodleServer && moodleServer.release) { + var matches = moodleServer.release.match(/^\s*(\d+\.\d+)/); + if (matches && matches[1]) { + moodleInfo.release = parseFloat(matches[1]); + } + } + + if (typeof window.qf_errorHandler === 'function' && config.tinymce && + !config.tinymce.onSubmitUnsaved) { + config.tinymce.onSubmitUnsaved = this.onSubmitUnsaved; + } + + return true; + }; + + /** + * The submit event handler for the form to which the PaintWeb + * instance is attached to. This method is invoked by the TinyMCE plugin when + * the form is submitted while the user edits an image with unsaved changes. + * @private + */ + this.onSubmitUnsaved = function () { + var tmce = config.tinymceEditor, + textarea = tmce ? tmce.getElement() : null, + guiPlaceholder = config.guiPlaceholder, + prevSibling = guiPlaceholder.previousSibling; + + if (tmce && textarea && window.qf_errorHandler) { + try { + qf_errorHandler(textarea, "\n - " + lang.errorSubmitUnsaved); + } catch (err) { + return; + } + + qfErrorShown = true; + + // Due to the styling of the error shown by Moodle, PaintWeb must have + // clear:right. + if (prevSibling && prevSibling.className && + prevSibling.className.indexOf('paintweb_tinymce_status') !== -1) { + prevSibling.style.clear = 'right'; + } else { + guiPlaceholder.style.clear = 'right'; + } + } + }; + + /** + * The imageSave application event handler. When the user + * attempts to save an image, this extension handles the event by sending the + * image data to the Moodle server, to perform the actual save operation. + * + * @private + * @param {pwlib.appEvent.imageSave} ev The application event object. + */ + this.imageSave = function (ev) { + if (!ev.dataURL) { + return; + } + + ev.preventDefault(); + + moodleInfo.imageURL = config.imageLoad.src; + if (!moodleInfo.imageURL || moodleInfo.imageURL.substr(0, 5) === 'data:') { + moodleInfo.imageURL = '-'; + } + + if (config.moodleSaveMethod === 'dataURL') { + app.events.dispatch(new appEvent.imageSaveResult(true, + moodleInfo.imageURL, ev.dataURL)); + + } else { + var handlerURL = PaintWeb.baseFolder, + send = 'url=' + encodeURIComponent(moodleInfo.imageURL) + + '&dataURL=' + encodeURIComponent(ev.dataURL), + headers = {'Content-Type': 'application/x-www-form-urlencoded'}; + + // In Moodle 2.0 we include the context ID and the draft item ID, such + // that the image save script can properly save the new image inside the + // current draft area of the current textarea element. + if (moodleInfo.release >= 2) { + handlerURL += moodleInfo.imageSaveHandler20; + if (moodleServer.contextid) { + send += '&contextid=' + encodeURIComponent(moodleServer.contextid); + } + if (moodleInfo.draftitemid) { + send += '&draftitemid=' + encodeURIComponent(moodleInfo.draftitemid); + } + + } else { + handlerURL += moodleInfo.imageSaveHandler19; + } + + pwlib.xhrLoad(handlerURL, imageSaveReady, 'POST', send, headers); + } + }; + + /** + * The image save onreadystatechange event handler for the + * XMLHttpRequest which performs the image save. This function + * uses the reply to determine if the image save operation is successful or + * not. + * + *

The {@link pwlib.appEvent.imageSaveResult} application event is + * dispatched. + * + *

The server-side script must reply with a JSON object with the following + * properties: + * + *

+ * + * @private + * @param {XMLHttpRequest} xhr The XMLHttpRequest object. + */ + function imageSaveReady (xhr) { + if (!xhr || xhr.readyState !== 4) { + return; + } + + var result = {successful: false, url: moodleInfo.imageURL}; + + if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { + alert(lang.xhrRequestFailed); + + app.events.dispatch(new appEvent.imageSaveResult(false, result.url, null, + lang.xhrRequestFailed)); + + return; + } + + try { + result = JSON.parse(xhr.responseText); + } catch (err) { + result.errorMessage = lang.jsonParseFailed + "\n" + err; + alert(result.errorMessage); + } + + if (result.successful) { + if (result.url !== moodleInfo.imageURL) { + alert(pwlib.strf(lang.urlMismatch, { + url: moodleInfo.imageURL, + urlServer: result.url || 'null'})); + } + } else { + if (result.errorMessage) { + alert(lang.imageSaveFailed + "\n" + result.errorMessage); + } else { + alert(lang.imageSaveFailed); + } + } + + app.events.dispatch(new appEvent.imageSaveResult(result.successful, + result.url, result.urlNew, result.errorMessage)); + }; + + /** + * The guiShow application event handler. When the PaintWeb GUI + * is shown, we must hide the textarea icons for the current textarea element, + * inside a Moodle page. + * @private + */ + this.guiShow = function () { + var pNode = config.guiPlaceholder.parentNode, + textareaButtons + = pNode.getElementsByClassName(moodleInfo.textareaButtons)[0]; + + // These show in Moodle 1.9. + if (textareaButtons) { + textareaButtons.style.display = 'none'; + } + + qfErrorShown = false; + + // For Moodle 2.0 we must determine the draft item ID in order to properly + // perform the image save operation into the current draft area. + if (moodleInfo.release < 2) { + return; + } + + // Typically the TinyMCE editor instance is attached to a textarea element + // which has a name=whatever[text] or similar form. In the same form as the + // textarea, there must be a hidden input element with the + // name=whatever[itemid]. The value of that input holds the draft item ID. + var tmce = config.tinymceEditor, + textarea = tmce ? tmce.getElement() : null, + frm = textarea ? textarea.form : null; + + if (!tmce || !textarea || !textarea.name || !frm) { + return; + } + + var fieldname = textarea.name.replace(/\[text\]$/, ''); + if (!fieldname) { + return; + } + + var draftitemid = frm.elements.namedItem(fieldname + '[itemid]'), + format = frm.elements.namedItem(fieldname + '[format]'); + + if (draftitemid) { + moodleInfo.draftitemid = draftitemid.value; + } + + if (format) { + format.style.display = 'none'; + } + }; + + /** + * The guiHide application event handler. When the PaintWeb GUI + * is hidden, we must show again the textarea icons for the current textarea + * element, inside a Moodle page. + * @private + */ + this.guiHide = function () { + var guiPlaceholder = config.guiPlaceholder, + prevSibling = guiPlaceholder.previousSibling; + pNode = guiPlaceholder.parentNode, + textareaButtons + = pNode.getElementsByClassName(moodleInfo.textareaButtons)[0]; + + // These show in Moodle 1.9. + if (textareaButtons) { + textareaButtons.style.display = ''; + } + + var tmce = config.tinymceEditor, + textarea = tmce ? tmce.getElement() : null, + frm = textarea ? textarea.form : null; + + if (!tmce || !textarea || !textarea.name || !frm) { + return; + } + + if (qfErrorShown) { + if (window.qf_errorHandler) { + qf_errorHandler(textarea, ''); + } + + if (prevSibling && prevSibling.className && + prevSibling.className.indexOf('paintweb_tinymce_status') !== -1) { + prevSibling.style.clear = ''; + } else { + guiPlaceholder.style.clear = ''; + } + } + + // The format input element only shows in Moodle 2.0. + if (moodleInfo.release >= 2) { + var fieldname = textarea.name.replace(/\[text\]$/, ''); + if (!fieldname) { + return; + } + + var format = frm.elements.namedItem(fieldname + '[format]'); + + if (format) { + format.style.display = ''; + } + } + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/mousekeys.js b/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/mousekeys.js new file mode 100644 index 00000000..fca2e836 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/extensions/mousekeys.js @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-16 21:56:40 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Allows users to draw in PaintWeb using the keyboard, without + * any pointing device. + */ + +/** + * @class The MouseKeys extension. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.extensions.mousekeys = function (app) { + var _self = this, + canvas = app.buffer.canvas, + config = app.config, + container = app.gui.elems.canvasContainer, + doc = app.doc, + gui = app.gui, + image = app.image, + MathCeil = Math.ceil, + mouse = app.mouse, + tool = app.tool || {}; + + /** + * Holds the current mouse movement speed in pixels. + * + * @private + * @type Number + */ + var speed = 1; + + /** + * Holds the current mouse movement acceleration, taken from the + * configuration. + * + * @private + * @type Number + * @see PaintWeb.config.mousekeys.accel The mouse keys acceleration setting. + */ + var accel = 0.1; + + /** + * Holds a reference to the DOM element representing the pointer on top of the + * canvas element. + * + * @private + * @type Element + */ + var pointer = null; + var pointerStyle = null; + + /** + * The extensionRegister event handler. This initializes the + * extension by adding the pointer DOM element and by setting up the keyboard + * shortcuts. + * + * @returns {Boolean} True if the extension initialized successfully, or false + * if not. + */ + this.extensionRegister = function () { + accel = config.mousekeys.accel; + + pointer = doc.createElement('div'); + if (!pointer) { + return false; + } + pointerStyle = pointer.style; + + pointer.className = gui.classPrefix + 'mousekeysPointer'; + pointerStyle.display = 'none'; + container.appendChild(pointer); + + canvas.addEventListener('mousemove', pointerMousemove, false); + + var action, keys, i, n, result = {}; + + for (action in config.mousekeys.actions) { + keys = config.mousekeys.actions[action]; + + for (i = 0, n = keys.length; i < n; i++) { + result[keys[i]] = {'extension': _self._id, 'action': action}; + } + }; + + pwlib.extend(config.keys, result); + + return true; + }; + + /** + * The extensionUnregister event handler. This will remove the + * pointer DOM element and the canvas event listener. + */ + this.extensionUnregister = function () { + container.removeChild(pointer); + canvas.removeEventListener('mousemove', pointerMousemove, false); + + var key, kobj; + for (key in config.keys) { + kobj = config.keys[key]; + if (kobj.extension === _self._id) { + delete config.keys[key]; + } + } + }; + + /** + * Track the virtual pointer coordinates, by updating the position of the + * pointer element. This allows the keyboard users to see where + * they moved the virtual pointer. + * + * @param {Event} ev The DOM Event object. + */ + function pointerMousemove (ev) { + if (!('kobj_' in ev) || !('extension' in ev.kobj_) || + ev.kobj_.extension !== _self._id) { + if (pointerStyle.display === 'block') { + pointerStyle.display = 'none'; + } + } + }; + + /** + * The keydown event handler. + * + *

This method requires a DOM Event object which has the + * ev.kobj_ object reference from the keyboard shortcuts + * configuration. The kobj_ object must have the action + * property. Support for the "ButtonToggle" and the "ButtonClick" actions is + * implemented. + * + *

The "ButtonToggle" action essentially means that a mouse event will be + * generated, either mousedown or mouseup. By + * alternating these two events, this method allows the user to start and stop + * the drawing operation at any moment using the keyboard shortcut they have + * configured. + * + *

Under typical usage, the "ButtonClick" action translates the + * keydown event to mousedown. The + * keyup event handler will also fire the mouseup + * event. This allows the user to simulate holding down the mouse button, + * while he/she holds down a key. + * + *

A click event is always fired after the firing of + * a mouseup event. + * + *

Irrespective of the key the user pressed, this method does always reset + * the speed and acceleration of the pointer movement. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + * + * @see PaintWeb.config.mousekeys.actions The keyboard shortcuts configuration + * object. + */ + this.keydown = function (ev) { + speed = 1; + accel = config.mousekeys.accel; + + if (pointerStyle.display === 'none') { + pointerStyle.display = 'block'; + pointerStyle.top = (mouse.y * image.canvasScale) + 'px'; + pointerStyle.left = (mouse.x * image.canvasScale) + 'px'; + + if (mouse.buttonDown) { + pointer.className += ' ' + gui.classPrefix + 'mouseDown'; + } else { + pointer.className = pointer.className.replace(' ' + gui.classPrefix + + 'mouseDown', ''); + } + } + + tool = app.tool || {}; + + switch (ev.kobj_.action) { + case 'ButtonToggle': + if (mouse.buttonDown) { + mouse.buttonDown = false; + if ('mouseup' in tool) { + tool.mouseup(ev); + } + if ('click' in tool) { + tool.click(ev); + } + + } else { + mouse.buttonDown = true; + + if ('mousedown' in tool) { + tool.mousedown(ev); + } + } + break; + + case 'ButtonClick': + if (!mouse.buttonDown) { + mouse.buttonDown = true; + + if ('mousedown' in tool) { + tool.mousedown(ev); + } + } + + break; + + default: + return false; + } + + if (mouse.buttonDown) { + pointer.className += ' ' + gui.classPrefix + 'mouseDown'; + } else { + pointer.className = pointer.className.replace(' ' + gui.classPrefix + + 'mouseDown', ''); + } + + return true; + }; + + /** + * The keypress event handler. + * + *

This method requires a DOM Event object with a ev.kobj_ + * object reference to the keyboard shortcut configuration. The keyboard + * shortcut configuration object must have the action property. + * + *

This event handler implements support for the following param + * values: "SouthWest", "South", "SouthEast", "West", "East", "NorthWest", + * "North" and "NorthEast", All of these values indicate the movement + * direction. This method generates synthetic movemove events based + * on the direction desired, effectively emulating the use of a real pointing + * device. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + * + * @see PaintWeb.config.mousekeys.actions The keyboard shortcuts configuration + * object. + */ + this.keypress = function (ev) { + if (ev.shiftKey) { + speed += speed * accel * 3; + } else { + speed += speed * accel; + } + + var step = MathCeil(speed); + + switch (ev.kobj_.action) { + case 'SouthWest': + mouse.x -= step; + mouse.y += step; + break; + case 'South': + mouse.y += step; + break; + case 'SouthEast': + mouse.x += step; + mouse.y += step; + break; + case 'West': + mouse.x -= step; + break; + case 'East': + mouse.x += step; + break; + case 'NorthWest': + mouse.x -= step; + mouse.y -= step; + break; + case 'North': + mouse.y -= step; + break; + case 'NorthEast': + mouse.x += step; + mouse.y -= step; + break; + default: + return false; + } + + if (mouse.x < 0) { + mouse.x = 0; + } else if (mouse.x > image.width) { + mouse.x = image.width; + } + + if (mouse.y < 0) { + mouse.y = 0; + } else if (mouse.y > image.height) { + mouse.y = image.height; + } + + pointerStyle.top = (mouse.y * image.canvasScale) + 'px'; + pointerStyle.left = (mouse.x * image.canvasScale) + 'px'; + + if ('mousemove' in tool) { + tool.mousemove(ev); + } + + return true; + }; + + /** + * The keyup event handler. + * + *

This method requires a DOM Event object which has the + * ev.kobj_ object reference from the keyboard shortcuts + * configuration. The kobj_ object must have the action + * property. Support for the "ButtonClick" action is implemented. + * + *

Under typical usage, the "ButtonClick" action translates the + * keydown event to mousedown. This event handler + * fires the mouseup event. This allows the user to simulate + * holding down the mouse button, while he/she holds down a key. + * + *

A click event is always fired after the firing of the + * mouseup event. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + * + * @see PaintWeb.config.mousekeys.actions The keyboard shortcuts configuration + * object. + */ + this.keyup = function (ev) { + if (ev.kobj_.action == 'ButtonClick' && mouse.buttonDown) { + mouse.buttonDown = false; + + if ('mouseup' in tool) { + tool.mouseup(ev); + } + if ('click' in tool) { + tool.click(ev); + } + + pointer.className = pointer.className.replace(' ' + gui.classPrefix + + 'mouseDown', ''); + return true; + } + + return false; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/html2canvas.min.js b/src/main/webapp/resources/lib/fims/biz/paintweb/html2canvas.min.js new file mode 100644 index 00000000..be7d72af --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/html2canvas.min.js @@ -0,0 +1,6 @@ +/*! + * html2canvas 1.0.0-alpha.11 + * Copyright (c) 2018 Niklas von Hertzen + * Released under MIT License + */ +!function(A,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.html2canvas=e():A.html2canvas=e()}(this,function(){return function(A){var e={};function t(r){if(e[r])return e[r].exports;var n=e[r]={i:r,l:!1,exports:{}};return A[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}return t.m=A,t.c=e,t.d=function(A,e,r){t.o(A,e)||Object.defineProperty(A,e,{configurable:!1,enumerable:!0,get:r})},t.n=function(A){var e=A&&A.__esModule?function(){return A.default}:function(){return A};return t.d(e,"a",e),e},t.o=function(A,e){return Object.prototype.hasOwnProperty.call(A,e)},t.p="",t(t.s=27)}([function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){return function(A,e){if(Array.isArray(A))return A;if(Symbol.iterator in Object(A))return function(A,e){var t=[],r=!0,n=!1,B=void 0;try{for(var a,s=A[Symbol.iterator]();!(r=(a=s.next()).done)&&(t.push(a.value),!e||t.length!==e);r=!0);}catch(A){n=!0,B=A}finally{try{!r&&s.return&&s.return()}finally{if(n)throw B}}return t}(A,e);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),n=function(){function A(A,e){for(var t=0;t4)&&[Number(e[1]),Number(e[2]),Number(e[3]),Number(e[4])]},Q=function(A){return[Math.min(A[0],255),Math.min(A[1],255),Math.min(A[2],255),A.length>3?A[3]:null]},w=function(A){var e=g[A.toLowerCase()];return e||!1},U=function(){function A(e){!function(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}(this,A);var t=Array.isArray(e)?Q(e):a(e)||c(e)||u(e)||w(e)||o(e)||[0,0,0,null],n=r(t,4),B=n[0],s=n[1],i=n[2],l=n[3];this.r=B,this.g=s,this.b=i,this.a=l}return n(A,[{key:"isTransparent",value:function(){return 0===this.a}},{key:"toString",value:function(){return null!==this.a&&1!==this.a?"rgba("+this.r+","+this.g+","+this.b+","+this.a+")":"rgb("+this.r+","+this.g+","+this.b+")"}}]),A}();e.default=U;var g={transparent:[0,0,0,0],aliceblue:[240,248,255,null],antiquewhite:[250,235,215,null],aqua:[0,255,255,null],aquamarine:[127,255,212,null],azure:[240,255,255,null],beige:[245,245,220,null],bisque:[255,228,196,null],black:[0,0,0,null],blanchedalmond:[255,235,205,null],blue:[0,0,255,null],blueviolet:[138,43,226,null],brown:[165,42,42,null],burlywood:[222,184,135,null],cadetblue:[95,158,160,null],chartreuse:[127,255,0,null],chocolate:[210,105,30,null],coral:[255,127,80,null],cornflowerblue:[100,149,237,null],cornsilk:[255,248,220,null],crimson:[220,20,60,null],cyan:[0,255,255,null],darkblue:[0,0,139,null],darkcyan:[0,139,139,null],darkgoldenrod:[184,134,11,null],darkgray:[169,169,169,null],darkgreen:[0,100,0,null],darkgrey:[169,169,169,null],darkkhaki:[189,183,107,null],darkmagenta:[139,0,139,null],darkolivegreen:[85,107,47,null],darkorange:[255,140,0,null],darkorchid:[153,50,204,null],darkred:[139,0,0,null],darksalmon:[233,150,122,null],darkseagreen:[143,188,143,null],darkslateblue:[72,61,139,null],darkslategray:[47,79,79,null],darkslategrey:[47,79,79,null],darkturquoise:[0,206,209,null],darkviolet:[148,0,211,null],deeppink:[255,20,147,null],deepskyblue:[0,191,255,null],dimgray:[105,105,105,null],dimgrey:[105,105,105,null],dodgerblue:[30,144,255,null],firebrick:[178,34,34,null],floralwhite:[255,250,240,null],forestgreen:[34,139,34,null],fuchsia:[255,0,255,null],gainsboro:[220,220,220,null],ghostwhite:[248,248,255,null],gold:[255,215,0,null],goldenrod:[218,165,32,null],gray:[128,128,128,null],green:[0,128,0,null],greenyellow:[173,255,47,null],grey:[128,128,128,null],honeydew:[240,255,240,null],hotpink:[255,105,180,null],indianred:[205,92,92,null],indigo:[75,0,130,null],ivory:[255,255,240,null],khaki:[240,230,140,null],lavender:[230,230,250,null],lavenderblush:[255,240,245,null],lawngreen:[124,252,0,null],lemonchiffon:[255,250,205,null],lightblue:[173,216,230,null],lightcoral:[240,128,128,null],lightcyan:[224,255,255,null],lightgoldenrodyellow:[250,250,210,null],lightgray:[211,211,211,null],lightgreen:[144,238,144,null],lightgrey:[211,211,211,null],lightpink:[255,182,193,null],lightsalmon:[255,160,122,null],lightseagreen:[32,178,170,null],lightskyblue:[135,206,250,null],lightslategray:[119,136,153,null],lightslategrey:[119,136,153,null],lightsteelblue:[176,196,222,null],lightyellow:[255,255,224,null],lime:[0,255,0,null],limegreen:[50,205,50,null],linen:[250,240,230,null],magenta:[255,0,255,null],maroon:[128,0,0,null],mediumaquamarine:[102,205,170,null],mediumblue:[0,0,205,null],mediumorchid:[186,85,211,null],mediumpurple:[147,112,219,null],mediumseagreen:[60,179,113,null],mediumslateblue:[123,104,238,null],mediumspringgreen:[0,250,154,null],mediumturquoise:[72,209,204,null],mediumvioletred:[199,21,133,null],midnightblue:[25,25,112,null],mintcream:[245,255,250,null],mistyrose:[255,228,225,null],moccasin:[255,228,181,null],navajowhite:[255,222,173,null],navy:[0,0,128,null],oldlace:[253,245,230,null],olive:[128,128,0,null],olivedrab:[107,142,35,null],orange:[255,165,0,null],orangered:[255,69,0,null],orchid:[218,112,214,null],palegoldenrod:[238,232,170,null],palegreen:[152,251,152,null],paleturquoise:[175,238,238,null],palevioletred:[219,112,147,null],papayawhip:[255,239,213,null],peachpuff:[255,218,185,null],peru:[205,133,63,null],pink:[255,192,203,null],plum:[221,160,221,null],powderblue:[176,224,230,null],purple:[128,0,128,null],rebeccapurple:[102,51,153,null],red:[255,0,0,null],rosybrown:[188,143,143,null],royalblue:[65,105,225,null],saddlebrown:[139,69,19,null],salmon:[250,128,114,null],sandybrown:[244,164,96,null],seagreen:[46,139,87,null],seashell:[255,245,238,null],sienna:[160,82,45,null],silver:[192,192,192,null],skyblue:[135,206,235,null],slateblue:[106,90,205,null],slategray:[112,128,144,null],slategrey:[112,128,144,null],snow:[255,250,250,null],springgreen:[0,255,127,null],steelblue:[70,130,180,null],tan:[210,180,140,null],teal:[0,128,128,null],thistle:[216,191,216,null],tomato:[255,99,71,null],turquoise:[64,224,208,null],violet:[238,130,238,null],wheat:[245,222,179,null],white:[255,255,255,null],whitesmoke:[245,245,245,null],yellow:[255,255,0,null],yellowgreen:[154,205,50,null]};e.TRANSPARENT=new U([0,0,0,0])},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.calculateLengthFromValueWithUnit=e.LENGTH_TYPE=void 0;var r,n=function(){function A(A,e){for(var t=0;t1&&(r/=U,B/=U,a/=U,s/=U,o/=U,l/=U,u/=U,Q/=U);var g=A.width-a,C=A.height-l,d=A.width-o,F=A.height-Q;return{topLeftOuter:r>0||B>0?c(A.left,A.top,r,B,i.TOP_LEFT):new n.default(A.left,A.top),topLeftInner:r>0||B>0?c(A.left+e[3].borderWidth,A.top+e[0].borderWidth,Math.max(0,r-e[3].borderWidth),Math.max(0,B-e[0].borderWidth),i.TOP_LEFT):new n.default(A.left+e[3].borderWidth,A.top+e[0].borderWidth),topRightOuter:a>0||s>0?c(A.left+g,A.top,a,s,i.TOP_RIGHT):new n.default(A.left+A.width,A.top),topRightInner:a>0||s>0?c(A.left+Math.min(g,A.width+e[3].borderWidth),A.top+e[0].borderWidth,g>A.width+e[3].borderWidth?0:a-e[3].borderWidth,s-e[0].borderWidth,i.TOP_RIGHT):new n.default(A.left+A.width-e[1].borderWidth,A.top+e[0].borderWidth),bottomRightOuter:o>0||l>0?c(A.left+d,A.top+C,o,l,i.BOTTOM_RIGHT):new n.default(A.left+A.width,A.top+A.height),bottomRightInner:o>0||l>0?c(A.left+Math.min(d,A.width-e[3].borderWidth),A.top+Math.min(C,A.height+e[0].borderWidth),Math.max(0,o-e[1].borderWidth),l-e[2].borderWidth,i.BOTTOM_RIGHT):new n.default(A.left+A.width-e[1].borderWidth,A.top+A.height-e[2].borderWidth),bottomLeftOuter:u>0||Q>0?c(A.left,A.top+F,u,Q,i.BOTTOM_LEFT):new n.default(A.left,A.top+A.height),bottomLeftInner:u>0||Q>0?c(A.left+e[3].borderWidth,A.top+F,Math.max(0,u-e[3].borderWidth),Q-e[2].borderWidth,i.BOTTOM_LEFT):new n.default(A.left+e[3].borderWidth,A.top+A.height-e[2].borderWidth)}},{TOP_LEFT:0,TOP_RIGHT:1,BOTTOM_RIGHT:2,BOTTOM_LEFT:3}),c=function(A,e,t,r,a){var s=(Math.sqrt(2)-1)/3*4,o=t*s,c=r*s,l=A+t,u=e+r;switch(a){case i.TOP_LEFT:return new B.default(new n.default(A,u),new n.default(A,u-c),new n.default(l-o,e),new n.default(l,e));case i.TOP_RIGHT:return new B.default(new n.default(A,e),new n.default(A+o,e),new n.default(l,u-c),new n.default(l,u));case i.BOTTOM_RIGHT:return new B.default(new n.default(l,e),new n.default(l,e+c),new n.default(A+o,u),new n.default(A,u));case i.BOTTOM_LEFT:default:return new B.default(new n.default(l,u),new n.default(l-o,u),new n.default(A,e+c),new n.default(A,e))}}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,n=function(){function A(A,e){for(var t=0;t0&&this.style.visibility===I.VISIBILITY.VISIBLE}},{key:"isAbsolutelyPositioned",value:function(){return this.style.position!==f.POSITION.STATIC&&this.style.position!==f.POSITION.RELATIVE}},{key:"isPositioned",value:function(){return this.style.position!==f.POSITION.STATIC}},{key:"isFloating",value:function(){return this.style.float!==u.FLOAT.NONE}},{key:"isRootElement",value:function(){return null===this.parent}},{key:"isTransformed",value:function(){return null!==this.style.transform}},{key:"isPositionedWithZIndex",value:function(){return this.isPositioned()&&!this.style.zIndex.auto}},{key:"isInlineLevel",value:function(){return(0,s.contains)(this.style.display,l.DISPLAY.INLINE)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_BLOCK)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_FLEX)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_GRID)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_LIST_ITEM)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_TABLE)}},{key:"isInlineBlockOrInlineTable",value:function(){return(0,s.contains)(this.style.display,l.DISPLAY.INLINE_BLOCK)||(0,s.contains)(this.style.display,l.DISPLAY.INLINE_TABLE)}}]),A}();e.default=S;var L=function(A,e){if(A instanceof A.ownerDocument.defaultView.SVGSVGElement||A instanceof SVGSVGElement){var t=new XMLSerializer;return e.loadImage("data:image/svg+xml,"+encodeURIComponent(t.serializeToString(A)))}switch(A.tagName){case"IMG":var r=A;return e.loadImage(r.currentSrc||r.src);case"CANVAS":var n=A;return e.loadCanvas(n);case"IFRAME":var B=A.getAttribute("data-html2canvas-internal-iframe-key");if(B)return B}return null}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.contains=function(A,e){return 0!=(A&e)},e.distance=function(A,e){return Math.sqrt(A*A+e*e)},e.copyCSSStyles=function(A,e){for(var t=A.length-1;t>=0;t--){var r=A.item(t);"content"!==r&&e.style.setProperty(r,A.getPropertyValue(r))}return e},e.SMALL_IMAGE="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.parseBackgroundImage=e.parseBackground=e.calculateBackgroundRepeatPath=e.calculateBackgroundPosition=e.calculateBackgroungPositioningArea=e.calculateBackgroungPaintingArea=e.calculateGradientBackgroundSize=e.calculateBackgroundSize=e.BACKGROUND_ORIGIN=e.BACKGROUND_CLIP=e.BACKGROUND_SIZE=e.BACKGROUND_REPEAT=void 0;var r=i(t(0)),n=i(t(1)),B=i(t(31)),a=i(t(7)),s=t(2),o=t(17);function i(A){return A&&A.__esModule?A:{default:A}}var c=e.BACKGROUND_REPEAT={REPEAT:0,NO_REPEAT:1,REPEAT_X:2,REPEAT_Y:3},l=e.BACKGROUND_SIZE={AUTO:0,CONTAIN:1,COVER:2,LENGTH:3},u=e.BACKGROUND_CLIP={BORDER_BOX:0,PADDING_BOX:1,CONTENT_BOX:2},Q=e.BACKGROUND_ORIGIN=u,w=function A(e){switch(function(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}(this,A),e){case"contain":this.size=l.CONTAIN;break;case"cover":this.size=l.COVER;break;case"auto":this.size=l.AUTO;break;default:this.value=new n.default(e)}},U=(e.calculateBackgroundSize=function(A,e,t){var r=0,n=0,a=A.size;if(a[0].size===l.CONTAIN||a[0].size===l.COVER){var s=t.width/t.height,o=e.width/e.height;return s0&&(A=n.substr(0,e).toLowerCase(),n=n.substr(e)),"none"!==(n=n.toLowerCase())&&t.push({prefix:A,method:n,args:r})}r=[],n=a=""};return A.split("").forEach(function(A){if(0!==s||!e.test(A)){switch(A){case'"':B?B===A&&(B=null):B=A;break;case"(":if(B)break;if(0===s)return void(s=1);o++;break;case")":if(B)break;if(1===s){if(0===o)return s=0,void i();o--}break;case",":if(B)break;if(0===s)return void i();if(1===s&&0===o&&!n.match(/^url$/i))return r.push(a.trim()),void(a="")}0===s?n+=A:a+=A}}),i(),t}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.PATH={VECTOR:0,BEZIER_CURVE:1,CIRCLE:2}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=t(6);e.default=function A(e,t){!function(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}(this,A),this.type=r.PATH.VECTOR,this.x=e,this.y=t}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.parseListStyle=e.parseListStyleType=e.LIST_STYLE_TYPE=e.LIST_STYLE_POSITION=void 0;var r=t(5),n=e.LIST_STYLE_POSITION={INSIDE:0,OUTSIDE:1},B=e.LIST_STYLE_TYPE={NONE:-1,DISC:0,CIRCLE:1,SQUARE:2,DECIMAL:3,CJK_DECIMAL:4,DECIMAL_LEADING_ZERO:5,LOWER_ROMAN:6,UPPER_ROMAN:7,LOWER_GREEK:8,LOWER_ALPHA:9,UPPER_ALPHA:10,ARABIC_INDIC:11,ARMENIAN:12,BENGALI:13,CAMBODIAN:14,CJK_EARTHLY_BRANCH:15,CJK_HEAVENLY_STEM:16,CJK_IDEOGRAPHIC:17,DEVANAGARI:18,ETHIOPIC_NUMERIC:19,GEORGIAN:20,GUJARATI:21,GURMUKHI:22,HEBREW:22,HIRAGANA:23,HIRAGANA_IROHA:24,JAPANESE_FORMAL:25,JAPANESE_INFORMAL:26,KANNADA:27,KATAKANA:28,KATAKANA_IROHA:29,KHMER:30,KOREAN_HANGUL_FORMAL:31,KOREAN_HANJA_FORMAL:32,KOREAN_HANJA_INFORMAL:33,LAO:34,LOWER_ARMENIAN:35,MALAYALAM:36,MONGOLIAN:37,MYANMAR:38,ORIYA:39,PERSIAN:40,SIMP_CHINESE_FORMAL:41,SIMP_CHINESE_INFORMAL:42,TAMIL:43,TELUGU:44,THAI:45,TIBETAN:46,TRAD_CHINESE_FORMAL:47,TRAD_CHINESE_INFORMAL:48,UPPER_ARMENIAN:49,DISCLOSURE_OPEN:50,DISCLOSURE_CLOSED:51},a=e.parseListStyleType=function(A){switch(A){case"disc":return B.DISC;case"circle":return B.CIRCLE;case"square":return B.SQUARE;case"decimal":return B.DECIMAL;case"cjk-decimal":return B.CJK_DECIMAL;case"decimal-leading-zero":return B.DECIMAL_LEADING_ZERO;case"lower-roman":return B.LOWER_ROMAN;case"upper-roman":return B.UPPER_ROMAN;case"lower-greek":return B.LOWER_GREEK;case"lower-alpha":return B.LOWER_ALPHA;case"upper-alpha":return B.UPPER_ALPHA;case"arabic-indic":return B.ARABIC_INDIC;case"armenian":return B.ARMENIAN;case"bengali":return B.BENGALI;case"cambodian":return B.CAMBODIAN;case"cjk-earthly-branch":return B.CJK_EARTHLY_BRANCH;case"cjk-heavenly-stem":return B.CJK_HEAVENLY_STEM;case"cjk-ideographic":return B.CJK_IDEOGRAPHIC;case"devanagari":return B.DEVANAGARI;case"ethiopic-numeric":return B.ETHIOPIC_NUMERIC;case"georgian":return B.GEORGIAN;case"gujarati":return B.GUJARATI;case"gurmukhi":return B.GURMUKHI;case"hebrew":return B.HEBREW;case"hiragana":return B.HIRAGANA;case"hiragana-iroha":return B.HIRAGANA_IROHA;case"japanese-formal":return B.JAPANESE_FORMAL;case"japanese-informal":return B.JAPANESE_INFORMAL;case"kannada":return B.KANNADA;case"katakana":return B.KATAKANA;case"katakana-iroha":return B.KATAKANA_IROHA;case"khmer":return B.KHMER;case"korean-hangul-formal":return B.KOREAN_HANGUL_FORMAL;case"korean-hanja-formal":return B.KOREAN_HANJA_FORMAL;case"korean-hanja-informal":return B.KOREAN_HANJA_INFORMAL;case"lao":return B.LAO;case"lower-armenian":return B.LOWER_ARMENIAN;case"malayalam":return B.MALAYALAM;case"mongolian":return B.MONGOLIAN;case"myanmar":return B.MYANMAR;case"oriya":return B.ORIYA;case"persian":return B.PERSIAN;case"simp-chinese-formal":return B.SIMP_CHINESE_FORMAL;case"simp-chinese-informal":return B.SIMP_CHINESE_INFORMAL;case"tamil":return B.TAMIL;case"telugu":return B.TELUGU;case"thai":return B.THAI;case"tibetan":return B.TIBETAN;case"trad-chinese-formal":return B.TRAD_CHINESE_FORMAL;case"trad-chinese-informal":return B.TRAD_CHINESE_INFORMAL;case"upper-armenian":return B.UPPER_ARMENIAN;case"disclosure-open":return B.DISCLOSURE_OPEN;case"disclosure-closed":return B.DISCLOSURE_CLOSED;case"none":default:return B.NONE}},s=(e.parseListStyle=function(A){var e=(0,r.parseBackgroundImage)(A.getPropertyValue("list-style-image"));return{listStyleType:a(A.getPropertyValue("list-style-type")),listStyleImage:e.length?e[0]:null,listStylePosition:s(A.getPropertyValue("list-style-position"))}},function(A){switch(A){case"inside":return n.INSIDE;case"outside":default:return n.OUTSIDE}})},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function A(A,e){for(var t=0;t0?e+t.toUpperCase():A}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=t(23),n=function(A){return 0===A[0]&&255===A[1]&&0===A[2]&&255===A[3]},B={get SUPPORT_RANGE_BOUNDS(){var A=function(A){if(A.createRange){var e=A.createRange();if(e.getBoundingClientRect){var t=A.createElement("boundtest");t.style.height="123px",t.style.display="block",A.body.appendChild(t),e.selectNode(t);var r=e.getBoundingClientRect(),n=Math.round(r.height);if(A.body.removeChild(t),123===n)return!0}}return!1}(document);return Object.defineProperty(B,"SUPPORT_RANGE_BOUNDS",{value:A}),A},get SUPPORT_SVG_DRAWING(){var A=function(A){var e=new Image,t=A.createElement("canvas"),r=t.getContext("2d");e.src="data:image/svg+xml,";try{r.drawImage(e,0,0),t.toDataURL()}catch(A){return!1}return!0}(document);return Object.defineProperty(B,"SUPPORT_SVG_DRAWING",{value:A}),A},get SUPPORT_BASE64_DRAWING(){return function(A){var e=function(A,e){var t=new Image,r=A.createElement("canvas"),n=r.getContext("2d");return new Promise(function(A){t.src=e;var B=function(){try{n.drawImage(t,0,0),r.toDataURL()}catch(e){return A(!1)}return A(!0)};t.onload=B,t.onerror=function(){return A(!1)},!0===t.complete&&setTimeout(function(){B()},500)})}(document,A);return Object.defineProperty(B,"SUPPORT_BASE64_DRAWING",{value:function(){return e}}),e}},get SUPPORT_FOREIGNOBJECT_DRAWING(){var A="function"==typeof Array.from&&"function"==typeof window.fetch?function(A){var e=A.createElement("canvas");e.width=100,e.height=100;var t=e.getContext("2d");t.fillStyle="rgb(0, 255, 0)",t.fillRect(0,0,100,100);var B=new Image,a=e.toDataURL();B.src=a;var s=(0,r.createForeignObjectSVG)(100,100,0,0,B);return t.fillStyle="red",t.fillRect(0,0,100,100),(0,r.loadSerializedSVG)(s).then(function(e){t.drawImage(e,0,0);var B=t.getImageData(0,0,100,100).data;t.fillStyle="red",t.fillRect(0,0,100,100);var s=A.createElement("div");return s.style.backgroundImage="url("+a+")",s.style.height="100px",n(B)?(0,r.loadSerializedSVG)((0,r.createForeignObjectSVG)(100,100,0,0,s)):Promise.reject(!1)}).then(function(A){return t.drawImage(A,0,0),n(t.getImageData(0,0,100,100).data)}).catch(function(A){return!1})}(document):Promise.resolve(!1);return Object.defineProperty(B,"SUPPORT_FOREIGNOBJECT_DRAWING",{value:A}),A},get SUPPORT_CORS_IMAGES(){var A=void 0!==(new Image).crossOrigin;return Object.defineProperty(B,"SUPPORT_CORS_IMAGES",{value:A}),A},get SUPPORT_RESPONSE_TYPE(){var A="string"==typeof(new XMLHttpRequest).responseType;return Object.defineProperty(B,"SUPPORT_RESPONSE_TYPE",{value:A}),A},get SUPPORT_CORS_XHR(){var A="withCredentials"in new XMLHttpRequest;return Object.defineProperty(B,"SUPPORT_CORS_XHR",{value:A}),A}};e.default=B},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.parseTextDecoration=e.TEXT_DECORATION_LINE=e.TEXT_DECORATION=e.TEXT_DECORATION_STYLE=void 0;var r,n=t(0),B=(r=n)&&r.__esModule?r:{default:r};var a=e.TEXT_DECORATION_STYLE={SOLID:0,DOUBLE:1,DOTTED:2,DASHED:3,WAVY:4},s=e.TEXT_DECORATION={NONE:null},o=e.TEXT_DECORATION_LINE={UNDERLINE:1,OVERLINE:2,LINE_THROUGH:3,BLINK:4},i=function(A){switch(A){case"underline":return o.UNDERLINE;case"overline":return o.OVERLINE;case"line-through":return o.LINE_THROUGH}return o.BLINK};e.parseTextDecoration=function(A){var e,t="none"===(e=A.textDecorationLine?A.textDecorationLine:A.textDecoration)?null:e.split(" ").map(i);return null===t?s.NONE:{textDecorationLine:t,textDecorationColor:A.textDecorationColor?new B.default(A.textDecorationColor):null,textDecorationStyle:function(A){switch(A){case"double":return a.DOUBLE;case"dotted":return a.DOTTED;case"dashed":return a.DASHED;case"wavy":return a.WAVY}return a.SOLID}(A.textDecorationStyle)}}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.parseBorder=e.BORDER_SIDES=e.BORDER_STYLE=void 0;var r,n=t(0),B=(r=n)&&r.__esModule?r:{default:r};var a=e.BORDER_STYLE={NONE:0,SOLID:1},s=e.BORDER_SIDES={TOP:0,RIGHT:1,BOTTOM:2,LEFT:3},o=Object.keys(s).map(function(A){return A.toLowerCase()});e.parseBorder=function(A){return o.map(function(e){var t=new B.default(A.getPropertyValue("border-"+e+"-color")),r=function(A){switch(A){case"none":return a.NONE}return a.SOLID}(A.getPropertyValue("border-"+e+"-style")),n=parseFloat(A.getPropertyValue("border-"+e+"-width"));return{borderColor:t,borderStyle:r,borderWidth:isNaN(n)?0:n}})}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.toCodePoints=function(A){for(var e=[],t=0,r=A.length;t=55296&&n<=56319&&t>10),n%1024+56320)),(t+1===A||e.length>16384)&&(r+=String.fromCharCode.apply(String,e),e.length=0)}return r};for(var r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="undefined"==typeof Uint8Array?[]:new Uint8Array(256),B=0;B>4,l[B++]=(15&s)<<4|o>>2,l[B++]=(3&o)<<6|63&i;return c},e.polyUint16Array=function(A){for(var e=A.length,t=[],r=0;rt?F(A,n,B.length>0):r.integers.reduce(function(e,t,n){for(;A>=t;)A-=t,e+=r.values[n];return e},"")+B},U=function(A,e,t,r){var n="";do{t||A--,n=r(A)+n,A/=e}while(A*e>=e);return n},g=function(A,e,t,r,n){var B=t-e+1;return(A<0?"-":"")+(U(Math.abs(A),B,r,function(A){return(0,s.fromCodePoint)(Math.floor(A%B)+e)})+n)},C=function(A,e){var t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:". ",r=e.length;return U(Math.abs(A),r,!1,function(A){return e[Math.floor(A%r)]})+t},d=function(A,e,t,n,B,s){if(A<-9999||A>9999)return F(A,a.LIST_STYLE_TYPE.CJK_DECIMAL,B.length>0);var o=Math.abs(A),i=B;if(0===o)return e[0]+i;for(var c=0;o>0&&c<=4;c++){var l=o%10;0===l&&(0,r.contains)(s,1)&&""!==i?i=e[l]+i:l>1||1===l&&0===c||1===l&&1===c&&(0,r.contains)(s,2)||1===l&&1===c&&(0,r.contains)(s,4)&&A>100||1===l&&c>1&&(0,r.contains)(s,8)?i=e[l]+(c>0?t[c-1]:"")+i:1===l&&c>0&&(i=t[c-1]+i),o=Math.floor(o/10)}return(A<0?n:"")+i},F=e.createCounterText=function(A,e,t){var r=t?". ":"",n=t?"、":"",B=t?", ":"";switch(e){case a.LIST_STYLE_TYPE.DISC:return"•";case a.LIST_STYLE_TYPE.CIRCLE:return"◦";case a.LIST_STYLE_TYPE.SQUARE:return"◾";case a.LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO:var s=g(A,48,57,!0,r);return s.length<4?"0"+s:s;case a.LIST_STYLE_TYPE.CJK_DECIMAL:return C(A,"〇一二三四五六七八九",n);case a.LIST_STYLE_TYPE.LOWER_ROMAN:return w(A,1,3999,c,a.LIST_STYLE_TYPE.DECIMAL,r).toLowerCase();case a.LIST_STYLE_TYPE.UPPER_ROMAN:return w(A,1,3999,c,a.LIST_STYLE_TYPE.DECIMAL,r);case a.LIST_STYLE_TYPE.LOWER_GREEK:return g(A,945,969,!1,r);case a.LIST_STYLE_TYPE.LOWER_ALPHA:return g(A,97,122,!1,r);case a.LIST_STYLE_TYPE.UPPER_ALPHA:return g(A,65,90,!1,r);case a.LIST_STYLE_TYPE.ARABIC_INDIC:return g(A,1632,1641,!0,r);case a.LIST_STYLE_TYPE.ARMENIAN:case a.LIST_STYLE_TYPE.UPPER_ARMENIAN:return w(A,1,9999,l,a.LIST_STYLE_TYPE.DECIMAL,r);case a.LIST_STYLE_TYPE.LOWER_ARMENIAN:return w(A,1,9999,l,a.LIST_STYLE_TYPE.DECIMAL,r).toLowerCase();case a.LIST_STYLE_TYPE.BENGALI:return g(A,2534,2543,!0,r);case a.LIST_STYLE_TYPE.CAMBODIAN:case a.LIST_STYLE_TYPE.KHMER:return g(A,6112,6121,!0,r);case a.LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH:return C(A,"子丑寅卯辰巳午未申酉戌亥",n);case a.LIST_STYLE_TYPE.CJK_HEAVENLY_STEM:return C(A,"甲乙丙丁戊己庚辛壬癸",n);case a.LIST_STYLE_TYPE.CJK_IDEOGRAPHIC:case a.LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL:return d(A,"零一二三四五六七八九","十百千萬","負",n,14);case a.LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL:return d(A,"零壹貳參肆伍陸柒捌玖","拾佰仟萬","負",n,15);case a.LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL:return d(A,"零一二三四五六七八九","十百千萬","负",n,14);case a.LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL:return d(A,"零壹贰叁肆伍陆柒捌玖","拾佰仟萬","负",n,15);case a.LIST_STYLE_TYPE.JAPANESE_INFORMAL:return d(A,"〇一二三四五六七八九","十百千万","マイナス",n,0);case a.LIST_STYLE_TYPE.JAPANESE_FORMAL:return d(A,"零壱弐参四伍六七八九","拾百千万","マイナス",n,7);case a.LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL:return d(A,"영일이삼사오육칠팔구","십백천만","마이너스 ",B,7);case a.LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL:return d(A,"零一二三四五六七八九","十百千萬","마이너스 ",B,0);case a.LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL:return d(A,"零壹貳參四五六七八九","拾百千","마이너스 ",B,7);case a.LIST_STYLE_TYPE.DEVANAGARI:return g(A,2406,2415,!0,r);case a.LIST_STYLE_TYPE.GEORGIAN:return w(A,1,19999,Q,a.LIST_STYLE_TYPE.DECIMAL,r);case a.LIST_STYLE_TYPE.GUJARATI:return g(A,2790,2799,!0,r);case a.LIST_STYLE_TYPE.GURMUKHI:return g(A,2662,2671,!0,r);case a.LIST_STYLE_TYPE.HEBREW:return w(A,1,10999,u,a.LIST_STYLE_TYPE.DECIMAL,r);case a.LIST_STYLE_TYPE.HIRAGANA:return C(A,"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん");case a.LIST_STYLE_TYPE.HIRAGANA_IROHA:return C(A,"いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす");case a.LIST_STYLE_TYPE.KANNADA:return g(A,3302,3311,!0,r);case a.LIST_STYLE_TYPE.KATAKANA:return C(A,"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン",n);case a.LIST_STYLE_TYPE.KATAKANA_IROHA:return C(A,"イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス",n);case a.LIST_STYLE_TYPE.LAO:return g(A,3792,3801,!0,r);case a.LIST_STYLE_TYPE.MONGOLIAN:return g(A,6160,6169,!0,r);case a.LIST_STYLE_TYPE.MYANMAR:return g(A,4160,4169,!0,r);case a.LIST_STYLE_TYPE.ORIYA:return g(A,2918,2927,!0,r);case a.LIST_STYLE_TYPE.PERSIAN:return g(A,1776,1785,!0,r);case a.LIST_STYLE_TYPE.TAMIL:return g(A,3046,3055,!0,r);case a.LIST_STYLE_TYPE.TELUGU:return g(A,3174,3183,!0,r);case a.LIST_STYLE_TYPE.THAI:return g(A,3664,3673,!0,r);case a.LIST_STYLE_TYPE.TIBETAN:return g(A,3872,3881,!0,r);case a.LIST_STYLE_TYPE.DECIMAL:default:return g(A,48,57,!0,r)}}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function A(A,e){for(var t=0;tA.height?(A.left+=(A.width-A.height)/2,A.width=A.height):A.width0&&B){var a=e.ownerDocument.createElement("html2canvaswrapper");(0,c.copyCSSStyles)(e.ownerDocument.defaultView.getComputedStyle(e,null),a),a.style.position="absolute",a.style.left=t.bounds.left+"px",a.style.top=t.bounds.top+"px",n||(a.style.whiteSpace="nowrap");var s=e.ownerDocument.createTextNode(A);a.appendChild(s),B.appendChild(a),t.childNodes.push(r.default.fromTextNode(s,t)),B.removeChild(a)}}),h=function(A){var e="password"===A.type?new Array(A.value.length+1).join("•"):A.value;return 0===e.length?A.placeholder||"":e}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.parseTextBounds=e.TextBounds=void 0;var r,n=t(2),B=t(11),a=t(10),s=(r=a)&&r.__esModule?r:{default:r},o=t(24);var i=e.TextBounds=function A(e,t){!function(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}(this,A),this.text=e,this.bounds=t},c=(e.parseTextBounds=function(A,e,t){for(var r=0!==e.style.letterSpacing?(0,o.toCodePoints)(A).map(function(A){return(0,o.fromCodePoint)(A)}):(0,o.breakWords)(A,e),n=r.length,a=t.parentNode?t.parentNode.ownerDocument.defaultView:null,u=a?a.pageXOffset:0,Q=a?a.pageYOffset:0,w=[],U=0,g=0;g0)if(s.default.SUPPORT_RANGE_BOUNDS)w.push(new i(C,l(t,U,C.length,u,Q)));else{var d=t.splitText(C.length);w.push(new i(C,c(t,u,Q))),t=d}else s.default.SUPPORT_RANGE_BOUNDS||(t=t.splitText(C.length));U+=C.length}return w},function(A,e,t){var r=A.ownerDocument.createElement("html2canvaswrapper");r.appendChild(A.cloneNode(!0));var B=A.parentNode;if(B){B.replaceChild(r,A);var a=(0,n.parseBounds)(r,e,t);return r.firstChild&&B.replaceChild(r.firstChild,r),a}return new n.Bounds(0,0,0,0)}),l=function(A,e,t,r,B){var a=A.ownerDocument.createRange();return a.setStart(A,e),a.setEnd(A,e+t),n.Bounds.fromClientRect(a.getBoundingClientRect(),r,B)}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function A(A,e){for(var t=0;t0&&t.childNodes.push(B.default.fromTextNode(g,t));else if(g instanceof C.HTMLElement||g instanceof HTMLElement||C.parent&&g instanceof C.parent.HTMLElement){if(-1===c.indexOf(g.nodeName)){var d=new n.default(g,t,l,w++);if(d.isVisible()){"INPUT"===g.tagName?(0,a.inlineInputElement)(g,d):"TEXTAREA"===g.tagName?(0,a.inlineTextAreaElement)(g,d):"SELECT"===g.tagName?(0,a.inlineSelectElement)(g,d):d.style.listStyle&&d.style.listStyle.listStyleType!==o.LIST_STYLE_TYPE.NONE&&(0,s.inlineListItemElement)(g,d,l);var F="TEXTAREA"!==g.tagName,E=u(d,g);if(E||Q(d)){var f=E||d.isPositioned()?i.getRealParentStackingContext():i,h=new r.default(d,f,E);f.contexts.push(h),F&&A(g,d,h,l,w)}else i.children.push(d),F&&A(g,d,i,l,w)}}}else if(g instanceof C.SVGSVGElement||g instanceof SVGSVGElement||C.parent&&g instanceof C.parent.SVGSVGElement){var H=new n.default(g,t,l,w++),p=u(H,g);if(p||Q(H)){var N=p||H.isPositioned()?i.getRealParentStackingContext():i,I=new r.default(H,N,p);N.contexts.push(I)}else i.children.push(H)}}},u=function(A,e){return A.isRootElement()||A.isPositionedWithZIndex()||A.style.opacity<1||A.isTransformed()||w(A,e)},Q=function(A){return A.isPositioned()||A.isFloating()},w=function(A,e){return"BODY"===e.nodeName&&A.parent instanceof n.default&&A.parent.style.background.backgroundColor.isTransparent()}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,n=function(){function A(A,e){for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:"strict",t=[],r=[],n=[];return A.forEach(function(A,B){var a=O.get(A);if(a>c?(n.push(!0),a-=c):n.push(!1),-1!==["normal","auto","loose"].indexOf(e)&&-1!==[8208,8211,12316,12448].indexOf(A))return r.push(B),t.push(16);if(4===a||11===a){if(0===B)return r.push(B),t.push(N);var s=t[B-1];return-1===x.indexOf(s)?(r.push(r[B-1]),t.push(s)):(r.push(B),t.push(N))}return r.push(B),31===a?t.push("strict"===e?d:v):a===L?t.push(N):29===a?t.push(N):43===a?A>=131072&&A<=196605||A>=196608&&A<=262141?t.push(v):t.push(N):void t.push(a)}),[r,t,n]},G=function(A,e,t,r){var n=r[t];if(Array.isArray(A)?-1!==A.indexOf(n):A===n)for(var B=t;B<=r.length;){var a=r[++B];if(a===e)return!0;if(a!==l)break}if(n===l)for(var s=t;s>0;){var o=r[--s];if(Array.isArray(A)?-1!==A.indexOf(o):A===o)for(var i=t;i<=r.length;){var c=r[++i];if(c===e)return!0;if(c!==l)break}if(o!==l)break}return!1},Y=function(A,e){for(var t=A;t>=0;){var r=e[t];if(r!==l)return r;t--}return 0},W=function(A,e,t,r,n){if(0===t[r])return D;var B=r-1;if(Array.isArray(n)&&!0===n[B])return D;var a=B-1,s=B+1,o=e[B],i=a>=0?e[a]:0,c=e[s];if(2===o&&3===c)return D;if(-1!==P.indexOf(o))return _;if(-1!==P.indexOf(c))return D;if(-1!==X.indexOf(c))return D;if(8===Y(B,e))return M;if(11===O.get(A[B])&&(c===v||c===I||c===K))return D;if(7===o||7===c)return D;if(9===o)return D;if(-1===[l,u,Q].indexOf(o)&&9===c)return D;if(-1!==[w,U,g,E,p].indexOf(c))return D;if(Y(B,e)===F)return D;if(G(23,F,B,e))return D;if(G([w,U],d,B,e))return D;if(G(12,12,B,e))return D;if(o===l)return M;if(23===o||23===c)return D;if(16===c||16===o)return M;if(-1!==[u,Q,d].indexOf(c)||14===o)return D;if(36===i&&-1!==k.indexOf(o))return D;if(o===p&&36===c)return D;if(c===C&&-1!==R.concat(C,g,f,v,I,K).indexOf(o))return D;if(-1!==R.indexOf(c)&&o===f||-1!==R.indexOf(o)&&c===f)return D;if(o===H&&-1!==[v,I,K].indexOf(c)||-1!==[v,I,K].indexOf(o)&&c===h)return D;if(-1!==R.indexOf(o)&&-1!==z.indexOf(c)||-1!==z.indexOf(o)&&-1!==R.indexOf(c))return D;if(-1!==[H,h].indexOf(o)&&(c===f||-1!==[F,Q].indexOf(c)&&e[s+1]===f)||-1!==[F,Q].indexOf(o)&&c===f||o===f&&-1!==[f,p,E].indexOf(c))return D;if(-1!==[f,p,E,w,U].indexOf(c))for(var N=B;N>=0;){var L=e[N];if(L===f)return D;if(-1===[p,E].indexOf(L))break;N--}if(-1!==[H,h].indexOf(c))for(var x=-1!==[w,U].indexOf(o)?a:B;x>=0;){var J=e[x];if(J===f)return D;if(-1===[p,E].indexOf(J))break;x--}if(y===o&&-1!==[y,b,T,m].indexOf(c)||-1!==[b,T].indexOf(o)&&-1!==[b,S].indexOf(c)||-1!==[S,m].indexOf(o)&&c===S)return D;if(-1!==V.indexOf(o)&&-1!==[C,h].indexOf(c)||-1!==V.indexOf(c)&&o===H)return D;if(-1!==R.indexOf(o)&&-1!==R.indexOf(c))return D;if(o===E&&-1!==R.indexOf(c))return D;if(-1!==R.concat(f).indexOf(o)&&c===F||-1!==R.concat(f).indexOf(c)&&o===U)return D;if(41===o&&41===c){for(var W=t[B],j=1;W>0&&41===e[--W];)j++;if(j%2!=0)return D}return o===I&&c===K?D:M},j=(e.lineBreakAtIndex=function(A,e){if(0===e)return D;if(e>=A.length)return _;var t=J(A),r=B(t,2),n=r[0],a=r[1];return W(A,a,n,e)},function(A,e){e||(e={lineBreak:"normal",wordBreak:"normal"});var t=J(A,e.lineBreak),r=B(t,3),n=r[0],a=r[1],s=r[2];return"break-all"!==e.wordBreak&&"break-word"!==e.wordBreak||(a=a.map(function(A){return-1!==[f,N,L].indexOf(A)?v:A})),[n,a,"keep-all"===e.wordBreak?s.map(function(e,t){return e&&A[t]>=19968&&A[t]<=40959}):null]}),q=(e.inlineBreakOpportunities=function(A,e){var t=(0,i.toCodePoints)(A),r=D,n=j(t,e),a=B(n,3),s=a[0],o=a[1],c=a[2];return t.forEach(function(A,e){r+=(0,i.fromCodePoint)(A)+(e>=t.length-1?_:W(t,o,s,e+1,c))}),r},function(){function A(e,t,r,n){!function(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}(this,A),this._codePoints=e,this.required=t===_,this.start=r,this.end=n}return n(A,[{key:"slice",value:function(){return i.fromCodePoint.apply(void 0,function(A){if(Array.isArray(A)){for(var e=0,t=Array(A.length);e=c)return{done:!0};for(var A=D;u>B,c=e.UTRIE2_DATA_BLOCK_LENGTH=1<>B,Q=e.UTRIE2_INDEX_2_BMP_LENGTH=i+u,w=e.UTRIE2_UTF8_2B_INDEX_2_OFFSET=Q,U=e.UTRIE2_UTF8_2B_INDEX_2_LENGTH=32,g=e.UTRIE2_INDEX_1_OFFSET=w+U,C=e.UTRIE2_OMITTED_BMP_INDEX_1_LENGTH=65536>>a,d=e.UTRIE2_INDEX_2_BLOCK_LENGTH=1<=0){if(A<55296||A>56319&&A<=65535)return e=((e=this.index[A>>B])<>B)])<>a),e=this.index[e],e+=A>>B&F,e=((e=this.index[e])<0?t.width:r.width,B="number"==typeof t.height&&t.height>0?t.height:r.height;n>0&&B>0&&e.target.clip([(0,a.calculatePaddingBoxPath)(A.curvedBounds)],function(){e.target.drawImage(t,new a.Bounds(0,0,n,B),r)})}}},r=A.getClipPaths();r.length?this.target.clip(r,t):t()}},{key:"renderNodeBackgroundAndBorders",value:function(A){var e=this,t=!A.style.background.backgroundColor.isTransparent()||A.style.background.backgroundImage.length,r=A.style.border.some(function(A){return A.borderStyle!==l.BORDER_STYLE.NONE&&!A.borderColor.isTransparent()}),n=function(){var r=(0,c.calculateBackgroungPaintingArea)(A.curvedBounds,A.style.background.backgroundClip);t&&e.target.clip([r],function(){A.style.background.backgroundColor.isTransparent()||e.target.fill(A.style.background.backgroundColor),e.renderBackgroundImage(A)}),A.style.border.forEach(function(t,r){t.borderStyle===l.BORDER_STYLE.NONE||t.borderColor.isTransparent()||e.renderBorder(t,r,A.curvedBounds)})};if(t||r){var B=A.parent?A.parent.getClipPaths():[];B.length?this.target.clip(B,n):n()}}},{key:"renderBackgroundImage",value:function(A){var e=this;A.style.background.backgroundImage.slice(0).reverse().forEach(function(t){"url"===t.source.method&&t.source.args.length?e.renderBackgroundRepeat(A,t):/gradient/i.test(t.source.method)&&e.renderBackgroundGradient(A,t)})}},{key:"renderBackgroundRepeat",value:function(A,e){var t=this.options.imageStore.get(e.source.args[0]);if(t){var r=(0,c.calculateBackgroungPositioningArea)(A.style.background.backgroundOrigin,A.bounds,A.style.padding,A.style.border),n=(0,c.calculateBackgroundSize)(e,t,r),B=(0,c.calculateBackgroundPosition)(e.position,n,r),a=(0,c.calculateBackgroundRepeatPath)(e,B,n,r,A.bounds),s=Math.round(r.left+B.x),o=Math.round(r.top+B.y);this.target.renderRepeat(a,t,n,s,o)}}},{key:"renderBackgroundGradient",value:function(A,e){var t=(0,c.calculateBackgroungPositioningArea)(A.style.background.backgroundOrigin,A.bounds,A.style.padding,A.style.border),r=(0,c.calculateGradientBackgroundSize)(e,t),n=(0,c.calculateBackgroundPosition)(e.position,r,t),B=new a.Bounds(Math.round(t.left+n.x),Math.round(t.top+n.y),r.width,r.height),o=(0,s.parseGradient)(A,e.source,B);if(o)switch(o.type){case s.GRADIENT_TYPE.LINEAR_GRADIENT:this.target.renderLinearGradient(B,o);break;case s.GRADIENT_TYPE.RADIAL_GRADIENT:this.target.renderRadialGradient(B,o)}}},{key:"renderBorder",value:function(A,e,t){this.target.drawShape((0,a.parsePathForBorder)(t,e),A.borderColor)}},{key:"renderStack",value:function(A){var e=this;if(A.container.isVisible()){var t=A.getOpacity();t!==this._opacity&&(this.target.setOpacity(A.getOpacity()),this._opacity=t);var r=A.container.style.transform;null!==r?this.target.transform(A.container.bounds.left+r.transformOrigin[0].value,A.container.bounds.top+r.transformOrigin[1].value,r.transform,function(){return e.renderStackContent(A)}):this.renderStackContent(A)}}},{key:"renderStackContent",value:function(A){var e=w(A),t=n(e,5),r=t[0],B=t[1],a=t[2],s=t[3],o=t[4],i=Q(A),c=n(i,2),l=c[0],u=c[1];this.renderNodeBackgroundAndBorders(A.container),r.sort(U).forEach(this.renderStack,this),this.renderNodeContent(A.container),u.forEach(this.renderNode,this),s.forEach(this.renderStack,this),o.forEach(this.renderStack,this),l.forEach(this.renderNode,this),B.forEach(this.renderStack,this),a.sort(U).forEach(this.renderStack,this)}},{key:"render",value:function(A){this.options.backgroundColor&&this.target.rectangle(this.options.x,this.options.y,this.options.width,this.options.height,this.options.backgroundColor),this.renderStack(A);var e=this.target.getTarget();return e}}]),A}();e.default=u;var Q=function(A){for(var e=[],t=[],r=A.children.length,n=0;n0?r.push(o):t.push(o):o.container.isFloating()?n.push(o):B.push(o)}return[e,t,r,n,B]},U=function(A,e){return A.container.style.zIndex.order>e.container.style.zIndex.order?1:A.container.style.zIndex.ordere.container.index?1:-1}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.transformWebkitRadialGradientArgs=e.parseGradient=e.RadialGradient=e.LinearGradient=e.RADIAL_GRADIENT_SHAPE=e.GRADIENT_TYPE=void 0;var r=function(){return function(A,e){if(Array.isArray(A))return A;if(Symbol.iterator in Object(A))return function(A,e){var t=[],r=!0,n=!1,B=void 0;try{for(var a,s=A[Symbol.iterator]();!(r=(a=s.next()).done)&&(t.push(a.value),!e||t.length!==e);r=!0);}catch(A){n=!0,B=A}finally{try{!r&&s.return&&s.return()}finally{if(n)throw B}}return t}(A,e);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),n=(i(t(3)),t(53)),B=i(t(0)),a=t(1),s=i(a),o=t(4);function i(A){return A&&A.__esModule?A:{default:A}}function c(A,e){if(!(A instanceof e))throw new TypeError("Cannot call a class as a function")}var l=/^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i,u=/^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i,Q=/(px)|%|( 0)$/i,w=/^(from|to|color-stop)\((?:([\d.]+)(%)?,\s*)?(.+?)\)$/i,U=/^\s*(circle|ellipse)?\s*((?:([\d.]+)(px|r?em|%)\s*(?:([\d.]+)(px|r?em|%))?)|closest-side|closest-corner|farthest-side|farthest-corner)?\s*(?:at\s*(?:(left|center|right)|([\d.]+)(px|r?em|%))\s+(?:(top|center|bottom)|([\d.]+)(px|r?em|%)))?(?:\s|$)/i,g=e.GRADIENT_TYPE={LINEAR_GRADIENT:0,RADIAL_GRADIENT:1},C=e.RADIAL_GRADIENT_SHAPE={CIRCLE:0,ELLIPSE:1},d={left:new s.default("0%"),top:new s.default("0%"),center:new s.default("50%"),right:new s.default("100%"),bottom:new s.default("100%")},F=e.LinearGradient=function A(e,t){c(this,A),this.type=g.LINEAR_GRADIENT,this.colorStops=e,this.direction=t},E=e.RadialGradient=function A(e,t,r,n){c(this,A),this.type=g.RADIAL_GRADIENT,this.colorStops=e,this.shape=t,this.center=r,this.radius=n},f=(e.parseGradient=function(A,e,t){var r=e.args,n=e.method,B=e.prefix;return"linear-gradient"===n?h(r,t,!!B):"gradient"===n&&"linear"===r[0]?h(["to bottom"].concat(y(r.slice(3))),t,!!B):"radial-gradient"===n?H(A,"-webkit-"===B?v(r):r,t):"gradient"===n&&"radial"===r[0]?H(A,y(v(r.slice(1))),t):void 0},function(A,e,t){for(var r=[],n=e;nA.optimumDistance)?{optimumCorner:n,optimumDistance:B}:A},{optimumDistance:r?1/0:-1/0,optimumCorner:null}).optimumCorner},m=function(A,e,t,r,n){var B=t.x,a=t.y,s=0,i=0;switch(A){case"closest-side":e===C.CIRCLE?s=i=Math.min(Math.abs(B),Math.abs(B-n.width),Math.abs(a),Math.abs(a-n.height)):e===C.ELLIPSE&&(s=Math.min(Math.abs(B),Math.abs(B-n.width)),i=Math.min(Math.abs(a),Math.abs(a-n.height)));break;case"closest-corner":if(e===C.CIRCLE)s=i=Math.min((0,o.distance)(B,a),(0,o.distance)(B,a-n.height),(0,o.distance)(B-n.width,a),(0,o.distance)(B-n.width,a-n.height));else if(e===C.ELLIPSE){var c=Math.min(Math.abs(a),Math.abs(a-n.height))/Math.min(Math.abs(B),Math.abs(B-n.width)),l=T(n,B,a,!0);i=c*(s=(0,o.distance)(l.x-B,(l.y-a)/c))}break;case"farthest-side":e===C.CIRCLE?s=i=Math.max(Math.abs(B),Math.abs(B-n.width),Math.abs(a),Math.abs(a-n.height)):e===C.ELLIPSE&&(s=Math.max(Math.abs(B),Math.abs(B-n.width)),i=Math.max(Math.abs(a),Math.abs(a-n.height)));break;case"farthest-corner":if(e===C.CIRCLE)s=i=Math.max((0,o.distance)(B,a),(0,o.distance)(B,a-n.height),(0,o.distance)(B-n.width,a),(0,o.distance)(B-n.width,a-n.height));else if(e===C.ELLIPSE){var u=Math.max(Math.abs(a),Math.abs(a-n.height))/Math.max(Math.abs(B),Math.abs(B-n.width)),Q=T(n,B,a,!1);i=u*(s=(0,o.distance)(Q.x-B,(Q.y-a)/u))}break;default:s=r.x||0,i=void 0!==r.y?r.y:s}return{x:s,y:i}},v=e.transformWebkitRadialGradientArgs=function(A){var e="",t="",r="",n="",B=0,a=/^(left|center|right|\d+(?:px|r?em|%)?)(?:\s+(top|center|bottom|\d+(?:px|r?em|%)?))?$/i,s=/^\d+(px|r?em|%)?(?:\s+\d+(px|r?em|%)?)?$/i,o=A[B].match(a);o&&B++;var i=A[B].match(/^(circle|ellipse)?\s*(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)?$/i);i&&(e=i[1]||"","contain"===(r=i[2]||"")?r="closest-side":"cover"===r&&(r="farthest-corner"),B++);var c=A[B].match(s);c&&B++;var l=A[B].match(a);l&&B++;var u=A[B].match(s);u&&B++;var Q=l||o;Q&&Q[1]&&(n=Q[1]+(/^\d+$/.test(Q[1])?"px":""),Q[2]&&(n+=" "+Q[2]+(/^\d+$/.test(Q[2])?"px":"")));var w=u||c;return w&&(t=w[0],w[1]||(t+="px")),!n||e||t||r||(t=n,n=""),n&&(n="at "+n),[[e,r,t,n].filter(function(A){return!!A}).join(" ")].concat(A.slice(B))},y=function(A){return A.map(function(A){return A.match(w)}).map(function(e,t){if(!e)return A[t];switch(e[1]){case"from":return e[4]+" 0%";case"to":return e[4]+" 100%";case"color-stop":return"%"===e[3]?e[4]+" "+e[2]:e[4]+" "+100*parseFloat(e[2])+"%"}})}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=/([+-]?\d*\.?\d+)(deg|grad|rad|turn)/i;e.parseAngle=function(A){var e=A.match(r);if(e){var t=parseFloat(e[1]);switch(e[2].toLowerCase()){case"deg":return Math.PI*t/180;case"grad":return Math.PI/200*t;case"rad":return t;case"turn":return 2*Math.PI*t}}return null}},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.cloneWindow=e.DocumentCloner=void 0;var r=function(){return function(A,e){if(Array.isArray(A))return A;if(Symbol.iterator in Object(A))return function(A,e){var t=[],r=!0,n=!1,B=void 0;try{for(var a,s=A[Symbol.iterator]();!(r=(a=s.next()).done)&&(t.push(a.value),!e||t.length!==e);r=!0);}catch(A){n=!0,B=A}finally{try{!r&&s.return&&s.return()}finally{if(n)throw B}}return t}(A,e);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),n=function(){function A(A,e){for(var t=0;t1&&(t.backgroundColor=""),t.backgroundImage=A.join(",")}),A instanceof HTMLImageElement&&this.resourceLoader.inlineImage(A.src).then(function(e){if(e&&A instanceof HTMLImageElement&&A.parentNode){var t=A.parentNode,r=(0,o.copyCSSStyles)(A.style,e.cloneNode(!1));t.replaceChild(r,A)}}).catch(function(A){0})}}},{key:"inlineFonts",value:function(A){var e=this;return Promise.all(Array.from(A.styleSheets).map(function(e){return e.href?fetch(e.href).then(function(A){return A.text()}).then(function(A){return U(A,e.href)}).catch(function(A){return[]}):w(e,A)})).then(function(A){return A.reduce(function(A,e){return A.concat(e)},[])}).then(function(A){return Promise.all(A.map(function(A){return fetch(A.formats[0].src).then(function(A){return A.blob()}).then(function(A){return new Promise(function(e,t){var r=new FileReader;r.onerror=t,r.onload=function(){var A=r.result;e(A)},r.readAsDataURL(A)})}).then(function(e){return A.fontFace.setProperty("src",'url("'+e+'")'),"@font-face {"+A.fontFace.cssText+" "})}))}).then(function(t){var r=A.createElement("style");r.textContent=t.join("\n"),e.documentElement.appendChild(r)})}},{key:"createElementClone",value:function(A){var e=this;if(this.copyStyles&&A instanceof HTMLCanvasElement){var t=A.ownerDocument.createElement("img");try{return t.src=A.toDataURL(),t}catch(A){0}}if(A instanceof HTMLIFrameElement){var r=A.cloneNode(!1),n=N();r.setAttribute("data-html2canvas-internal-iframe-key",n);var a=(0,B.parseBounds)(A,0,0),s=a.width,i=a.height;return this.resourceLoader.cache[n]=K(A,this.options).then(function(A){return e.renderer(A,{async:e.options.async,allowTaint:e.options.allowTaint,backgroundColor:"#ffffff",canvas:null,imageTimeout:e.options.imageTimeout,logging:e.options.logging,proxy:e.options.proxy,removeContainer:e.options.removeContainer,scale:e.options.scale,foreignObjectRendering:e.options.foreignObjectRendering,useCORS:e.options.useCORS,target:new c.default,width:s,height:i,x:0,y:0,windowWidth:A.ownerDocument.defaultView.innerWidth,windowHeight:A.ownerDocument.defaultView.innerHeight,scrollX:A.ownerDocument.defaultView.pageXOffset,scrollY:A.ownerDocument.defaultView.pageYOffset},e.logger.child(n))}).then(function(e){return new Promise(function(t,n){var B=document.createElement("img");B.onload=function(){return t(e)},B.onerror=n,B.src=e.toDataURL(),r.parentNode&&r.parentNode.replaceChild((0,o.copyCSSStyles)(A.ownerDocument.defaultView.getComputedStyle(A),B),r)})}),r}if(A instanceof HTMLStyleElement&&A.sheet&&A.sheet.cssRules){var l=[].slice.call(A.sheet.cssRules,0).reduce(function(A,t){try{return t&&t.cssText?A+t.cssText:A}catch(r){return e.logger.log("Unable to access cssText property",t.name),A}},""),u=A.cloneNode(!1);return u.textContent=l,u}return A.cloneNode(!1)}},{key:"cloneNode",value:function(A){var e=A.nodeType===Node.TEXT_NODE?document.createTextNode(A.nodeValue):this.createElementClone(A),t=A.ownerDocument.defaultView,r=A instanceof t.HTMLElement?t.getComputedStyle(A):null,n=A instanceof t.HTMLElement?t.getComputedStyle(A,":before"):null,B=A instanceof t.HTMLElement?t.getComputedStyle(A,":after"):null;this.referenceElement===A&&e instanceof t.HTMLElement&&(this.clonedReferenceElement=e),e instanceof t.HTMLBodyElement&&h(e);for(var a=(0,l.parseCounterReset)(r,this.pseudoContentData),s=(0,l.resolvePseudoContent)(A,n,this.pseudoContentData),i=A.firstChild;i;i=i.nextSibling)i.nodeType===Node.ELEMENT_NODE&&("SCRIPT"===i.nodeName||i.hasAttribute("data-html2canvas-ignore")||"function"==typeof this.options.ignoreElements&&this.options.ignoreElements(i))||this.copyStyles&&"STYLE"===i.nodeName||e.appendChild(this.cloneNode(i));var c=(0,l.resolvePseudoContent)(A,B,this.pseudoContentData);if((0,l.popCounters)(a,this.pseudoContentData),A instanceof t.HTMLElement&&e instanceof t.HTMLElement)switch(n&&this.inlineAllImages(C(A,e,n,s,d)),B&&this.inlineAllImages(C(A,e,B,c,F)),!r||!this.copyStyles||A instanceof HTMLIFrameElement||(0,o.copyCSSStyles)(r,e),this.inlineAllImages(e),0===A.scrollTop&&0===A.scrollLeft||this.scrolledElements.push([e,A.scrollLeft,A.scrollTop]),A.nodeName){case"CANVAS":this.copyStyles||g(A,e);break;case"TEXTAREA":case"SELECT":e.value=A.value}return e}}]),A}(),w=function(A,e){return(A.cssRules?Array.from(A.cssRules):[]).filter(function(A){return A.type===CSSRule.FONT_FACE_RULE}).map(function(A){for(var t=(0,i.parseBackgroundImage)(A.style.getPropertyValue("src")),r=[],n=0;n0&&"complete"===t.readyState&&(clearInterval(e),r(A))},50)}})},v=(e.cloneWindow=function(A,e,t,r,n,B){var a=new Q(t,r,n,!1,B),s=A.defaultView.pageXOffset,o=A.defaultView.pageYOffset;return T(A,e).then(function(n){var B=n.contentWindow,i=B.document,c=m(n).then(function(){a.scrolledElements.forEach(p),B.scrollTo(e.left,e.top),!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)||B.scrollY===e.top&&B.scrollX===e.left||(i.documentElement.style.top=-e.top+"px",i.documentElement.style.left=-e.left+"px",i.documentElement.style.position="absolute");var t=Promise.resolve([n,a.clonedReferenceElement,a.resourceLoader]),s=r.onclone;return a.clonedReferenceElement instanceof B.HTMLElement||a.clonedReferenceElement instanceof A.defaultView.HTMLElement||a.clonedReferenceElement instanceof HTMLElement?"function"==typeof s?Promise.resolve().then(function(){return s(i)}).then(function(){return t}):t:Promise.reject("")});return i.open(),i.write(v(document.doctype)+""),function(A,e,t){!A.defaultView||e===A.defaultView.pageXOffset&&t===A.defaultView.pageYOffset||A.defaultView.scrollTo(e,t)}(t.ownerDocument,s,o),i.replaceChild(i.adoptNode(a.documentElement),i.documentElement),i.close(),c})},function(A){var e="";return A&&(e+=""),e})},function(A,e,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ResourceStore=void 0;var r,n=function(){function A(A,e){for(var t=0;t0&&t.push({type:s.ATTRIBUTE,value:l[0]});break;case"counter":if(l.length>0){var w={type:s.COUNTER,name:l[0]};l.length>1&&(w.format=l[1]),t.push(w)}break;case"counters":if(l.length>0){var U={type:s.COUNTERS,name:l[0]};l.length>1&&(U.glue=l[1]),l.length>2&&(U.format=l[2]),t.push(U)}break;case"url":l.length>0&&t.push({type:s.URL,value:l[0]})}a=!1,o=""}break;case",":n?o+=Q:a&&(l.push(o),o="");break;case" ":case"\t":n?o+=Q:o&&(i(t,o),o="");break;default:o+=Q}"\\"!==Q&&(B=!1)}return o&&i(t,o),e&&(e[A]=t),t}),i=function(A,e){switch(e){case"open-quote":A.push({type:s.OPENQUOTE});break;case"close-quote":A.push({type:s.CLOSEQUOTE})}},c=function(A,e,t){var r=A.quotes?A.quotes.split(/\s+/):["'\"'","'\"'"],n=2*t;return n>=r.length&&(n=r.length-2),e||++n,r[n].replace(/^["']|["']$/g,"")},l=function(A,e,t){for(var r=A.length,a="",s=0;s0&&(a+=e||""),a+=(0,n.createCounterText)(A[s],(0,B.parseListStyleType)(t||"decimal"),!1);return a}}])}); \ No newline at end of file diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/includes/debug.js b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/debug.js new file mode 100644 index 00000000..810e2c6b --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/debug.js @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-05-11 19:37:56 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Minimal code used for aiding debugging PaintWeb. + */ + +// Opera compatibility +if (!window.console) { + /** + * @namespace Holds a simple method used for debugging. Opera doesn't have the + * window.console API like Firefox+Firebug has. + */ + window.console = {}; +} + +if (!window.console.log) { + /** + * Display a message in the debugger. If available, opera.postError is used, + * otherwise no message is displayed. + * + * @param {mixed} mixed Any number of arguments, each one is displayed in the + * debugger. + */ + window.console.log = function () { + var msg = ''; + + for (var i = 0, n = arguments.length; i < n; i++) { + msg += arguments[i] + ' '; + } + + if (window.opera && window.opera.postError) { + opera.postError(msg); + } + }; +} + +if (!window.console.warn) { + /** + * Display a message in the debugger. If available, opera.postError is used, + * otherwise no warning is displayed. + * + * @param {mixed} mixed Any number of arguments, each one is displayed in the + * debugger. + */ + window.console.warn = function () { + console.log.apply(null, arguments); + }; +} + +/** + * JavaScript code performance profiler. + *

Nested timers are accounted for - see the example below. + * + * @example + * // To profile your code just do: + * var profiler = new $.profiler(); + * + * profiler.start('long test'); + * // ... more code ... + * profiler.start('function 1'); + * // ... more code ... + * profiler.stop('function 1'); + * // ... more code ... + * profiler.start('function 2'); + * // ... more code ... + * profiler.stop('function 2'); + * // ... more code ... + * profiler.stop('long test'); + * + * // To see the results do: + * profiler.reportText(); + * // or .. + * profiler.reportData(); + * + * @class JavaScript code performance profiler. + */ +var libProfiler = function () { + /** + * @ignore + * @class Function timer. This is the constructor used for instancing a single + * timer object created by the profiler. + * + * @private + * @param {String} name_ The timer name. + * @param {Boolean} [independent_=false] Tells if the timer is independent. + * Means this timer will not affect the timers execution stack. + */ + function fnTimer (name_, independent_) { + this.avgOwnTimePerCall = 0; + this.avgRunTimePerCall = 0; + this.calls = 0; + this.maxOwnTimePerCall = 0; + this.maxRunTimePerCall = 0; + this.minOwnTimePerCall = 0; + this.minRunTimePerCall = 0; + this.name = name_; + this.ownTimeTotal = 0; + this.runTimeTotal = 0; + this.state = fnTimer.STATE_NONE; + this.independent = independent_; + + var startTime_ = 0, + subTimerStart_ = 0, + subTime_ = 0, + stack_ = 0; + + /* + * Start timing function execution. + */ + this.start = function () { + if (this.state == fnTimer.STATE_START || + this.state == fnTimer.STATE_SUB) { + stack_++; + return; + } + + startTime_ = (new Date ()).getTime(); + this.state = fnTimer.STATE_START; + }; + + /* + * Stop timing function execution. + */ + this.stop = function () { + if (this.state == fnTimer.STATE_SUB) { + this.subTimerEnd(); + } + + if (this.state != fnTimer.STATE_START) { + return; + } + + this.calls++; + + if (stack_) { + stack_--; + return; + } + + var runTime = (new Date ()).getTime() - startTime_; + var ownTime = runTime - subTime_; + subTime_ = 0; + + this.runTimeTotal += runTime; + this.ownTimeTotal += ownTime; + + this.avgRunTimePerCall = this.runTimeTotal / this.calls; + this.avgOwnTimePerCall = this.ownTimeTotal / this.calls; + + if (runTime < this.minRunTimePerCall) { + this.minRunTimePerCall = runTime; + } + + if (runTime > this.maxRunTimePerCall) { + this.maxRunTimePerCall = runTime; + } + + if (ownTime < this.minOwnTimePerCall) { + this.minOwnTimePerCall = ownTime; + } + + if (ownTime > this.maxOwnTimePerCall) { + this.maxOwnTimePerCall = ownTime; + } + + this.state = fnTimer.STATE_STOP; + }; + + /* + * Start timing sub-function execution. The sub-function execution timer is + * used for calculating the ownTime (runTime - sub-function execution time). + */ + this.subTimerStart = function () { + if (this.independent || this.state != fnTimer.STATE_START) { + return; + } + + subTimerStart_ = (new Date()).getTime(); + + this.state = fnTimer.STATE_SUB; + }; + + /* + * Stop timing sub-function execution. + */ + this.subTimerEnd = function () { + if (this.independent || this.state != fnTimer.STATE_SUB) { + return; + } + + subTime_ += (new Date()).getTime() - subTimerStart_; + this.state = fnTimer.STATE_START; + }; + }; + + fnTimer.STATE_NONE = 0; + fnTimer.STATE_START = 1; + fnTimer.STATE_SUB = 2; + fnTimer.STATE_STOP = 3; + + /** + * Holds the timer objects. + */ + this.timers = {}; + + var activeTimer_ = null, + timersStack_ = []; + + /** + * Start/create a function timer. + * + * @param {String} name The timer name. + * @param {Boolean} [independent=false] Tells if the timer should be + * independent or not. This means that this new function timer is not be + * considered affecting the execution time of existing function timers in the + * call stack. + */ + this.start = function (name, independent) { + var timer = this.timers[name]; + if (!timer) { + timer = this.timers[name] = new fnTimer(name, independent); + } + + if (!timer.independent && activeTimer_ != name) { + var activeTimer = activeTimer_ ? this.timers[activeTimer_] : null; + + if (activeTimer && activeTimer.state == fnTimer.STATE_START) { + timersStack_.push(activeTimer_); + activeTimer.subTimerStart(); + } + + activeTimer_ = name; + } + + timer.start(); + }; + + /** + * Stop a function timer. + */ + this.stop = function (name) { + var timer = this.timers[name]; + if (!timer) { + return; + } + + timer.stop(); + + if (timer.independent || name != activeTimer_ || + name == activeTimer_ && timer.state == fnTimer.STATE_START) { + return; + } + + if (timersStack_.length > 0) { + activeTimer_ = timersStack_.pop(); + + var activeTimer = this.timers[activeTimer_]; + activeTimer.subTimerEnd(); + + } else { + activeTimer_ = null; + } + }; + + /** + * Generate timers report data. + * + * @returns {Object} Holds all the information gathered by the timers. + */ + this.reportData = function () { + var name, timer, timerDetails, + data = { + avgCallsPerTimer: 0, + avgOwnTimePerCall: 0, + avgOwnTimePerTimer: 0, + avgRunTimePerCall: 0, + avgRunTimePerTimer: 0, + callsTotal: 0, + maxCallsPerTimer: 0, + maxCallsPerTimerName: '', + maxOwnTimePerCall: 0, + maxOwnTimePerCallName: '', + maxRunTimePerCall: 0, + maxRunTimePerCallName: '', + minCallsPerTimer: 0, + minCallsPerTimerName: '', + minOwnTimePerCall: 0, + minOwnTimePerCallName: '', + minRunTimePerCall: 0, + minRunTimePerCallName: '', + ownTimeTotal: 0, + runTimeTotal: 0, + timers: 0, + timerDetails: [] + }; + + for (name in this.timers) { + timer = this.timers[name]; + if (timer.state != fnTimer.STATE_STOP) { + continue; + } + + timerDetails = { + name: name, + avgOwnTimePerCall: timer.avgOwnTimePerCall, + avgRunTimePerCall: timer.avgRunTimePerCall, + calls: timer.calls, + maxOwnTimePerCall: timer.maxOwnTimePerCall, + maxRunTimePerCall: timer.maxRunTimePerCall, + minOwnTimePerCall: timer.minOwnTimePerCall, + minRunTimePerCall: timer.minRunTimePerCall, + runTimeTotal: timer.runTimeTotal, + ownTimeTotal: timer.ownTimeTotal + }; + data.timerDetails.push(timerDetails); + + if (timer.calls > data.maxCallsPerTimer || !data.timers) { + data.maxCallsPerTimer = timer.calls; + data.maxCallsPerTimerName = name; + } + + if (timer.maxOwnTimePerCall > data.maxOwnTimePerCall || !data.timers) { + data.maxOwnTimePerCall = timer.maxOwnTimePerCall; + data.maxOwnTimePerCallName = name; + } + + if (timer.maxRunTimePerCall > data.maxRunTimePerCall || !data.timers) { + data.maxRunTimePerCall = timer.maxRunTimePerCall; + data.maxRunTimePerCallName = name; + } + + if (timer.calls < data.minCallsPerTimer || !data.timers) { + data.minCallsPerTimer = timer.calls; + data.minCallsPerTimerName = name; + } + + if (timer.minOwnTimePerCall < data.minOwnTimePerCall || !data.timers) { + data.minOwnTimePerCall = timer.minOwnTimePerCall; + data.minOwnTimePerCallName = name; + } + + if (timer.minRunTimePerCall < data.minRunTimePerCall || !data.timers) { + data.minRunTimePerCall = timer.minRunTimePerCall; + data.minRunTimePerCallName = name; + } + + data.runTimeTotal += timer.runTimeTotal; + data.ownTimeTotal += timer.ownTimeTotal; + data.callsTotal += timer.calls; + data.timers++; + } + + if (data.timers == 0) { + return data; + } + + data.avgCallsPerTimer = data.callsTotal / data.timers; + data.avgOwnTimePerCall = data.ownTimeTotal / data.callsTotal; + data.avgOwnTimePerTimer = data.ownTimeTotal / data.timers; + data.avgRunTimePerCall = data.runTimeTotal / data.callsTotal; + data.avgRunTimePerTimer = data.runTimeTotal / data.timers; + + return data; + }; + + /** + * Generate a report in text format. + * + * @returns {String} All the information gathered by the timers, as text. + */ + this.reportText = function () { + var data = this.reportData(), + timer, result = ''; + + if (!data.timers) { + return ''; + } + + for (var i = 0; i < data.timers; i++) { + timer = data.timerDetails[i]; + result += timer.name + ":\n" + + ' Avg ownTime / call: ' + timer.avgOwnTimePerCall + " ms\n" + + ' Avg runTime / call: ' + timer.avgRunTimePerCall + " ms\n" + + ' Calls: ' + timer.calls + "\n"+ + ' Max ownTime / call: ' + timer.maxOwnTimePerCall + " ms\n" + + ' Max runTime / call: ' + timer.maxRunTimePerCall + " ms\n" + + ' Min ownTime / call: ' + timer.minOwnTimePerCall + " ms\n" + + ' Min runTime / call: ' + timer.minRunTimePerCall + " ms\n" + + ' runTime: ' + timer.runTimeTotal + " ms\n" + + ' ownTime: ' + timer.ownTimeTotal + " ms\n\n"; + } + + result += "Overview info:\n" + + ' Avg calls / timer: ' + data.avgCallsPerTimer + "\n" + + ' Avg ownTime / call: ' + data.avgOwnTimePerCall + " ms\n" + + ' Avg ownTime / timer: ' + data.avgOwnTimePerTimer + " ms\n" + + ' Avg runTime / call: ' + data.avgRunTimePerCall + " ms\n" + + ' Avg runTime / timer: ' + data.avgRunTimePerTimer + " ms\n" + + ' Calls total: ' + data.callsTotal + "\n" + + ' Max calls / timer: ' + data.maxCallsPerTimer + ' (' + + data.maxCallsPerTimerName + ")\n" + + ' Max ownTime / call: ' + data.maxOwnTimePerCall + ' ms (' + + data.maxOwnTimePerCallName + ")\n" + + ' Max runTime / call: ' + data.maxRunTimePerCall + ' ms (' + + data.maxRunTimePerCallName + ")\n" + + ' Min calls / timer: ' + data.minCallsPerTimer + ' (' + + data.minCallsPerTimerName + ")\n" + + ' Min ownTime / call: ' + data.minOwnTimePerCall + ' ms (' + + data.minOwnTimePerCallName + ")\n" + + ' Min runTime / call: ' + data.minRunTimePerCall + ' ms (' + + data.minRunTimePerCallName + ")\n" + + ' Accumulated ownTime: ' + data.ownTimeTotal + " ms\n" + + ' Accumulated runTime: ' + data.runTimeTotal + " ms\n" + + ' Timers: ' + data.timers; + + return result; + }; + + /** + * Reset/clear all the timers. + */ + this.reset = function () { + this.timers = {}; + activeTimer_ = null; + timersStack_ = []; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/includes/json2.js b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/json2.js new file mode 100644 index 00000000..7e27df51 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/json2.js @@ -0,0 +1,478 @@ +/* + http://www.JSON.org/json2.js + 2009-04-16 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the object holding the key. + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. +*/ + +/*jslint evil: true */ + +/*global JSON */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + JSON = {}; +} +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/includes/lib.js b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/lib.js new file mode 100644 index 00000000..1e46e2a0 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/includes/lib.js @@ -0,0 +1,2617 @@ +/* + * Copyright (C) 2008, 2009, 2010 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2010-06-26 20:35:34 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Minimal JavaScript library which provides functionality for + * cross-browser compatibility support. + */ + +/** + * @namespace Holds methods and properties necessary throughout the entire + * application. + */ +var pwlib = {}; + +/** + * @namespace Holds pre-packaged files. + * @type Object + */ +pwlib.fileCache = {}; + +/** + * @namespace Holds the implementation of each drawing tool. + * + * @type Object + * + * @see PaintWeb#toolRegister Register a new drawing tool into a PaintWeb + * instance. + * @see PaintWeb#toolActivate Activate a drawing tool in a PaintWeb instance. + * @see PaintWeb#toolUnregister Unregister a drawing tool from a PaintWeb + * instance. + * + * @see PaintWeb.config.toolDefault The default tool being activated when + * a PaintWeb instance is initialized. + * @see PaintWeb.config.tools Holds the list of tools to be loaded automatically + * when a PaintWeb instance is initialized. + */ +pwlib.tools = {}; + +/** + * @namespace Holds all the PaintWeb extensions. + * + * @type Object + * @see PaintWeb#extensionRegister Register a new extension into a PaintWeb + * instance. + * @see PaintWeb#extensionUnregister Unregister an extension from a PaintWeb + * instance. + * @see PaintWeb.config.extensions Holds the list of extensions to be loaded + * automatically when a PaintWeb instance is initialized. + */ +pwlib.extensions = {}; + +/** + * This function extends objects. + * + * @example + * var obj1 = {a: 'a1', b: 'b1', d: 'd1'}, + * obj2 = {a: 'a2', b: 'b2', c: 'c2'}; + * + * pwlib.extend(obj1, obj2); + * + * // Now obj1.c == 'c2', while obj1.a, obj1.b + * // and obj1.d remain the same. + * + * // If pwlib.extend(true, obj1, obj2) is + * // called, then obj1.a, obj1.b, obj1.c + * // become all the same as in obj2. + * + * @example + * var obj1 = {a: 'a1', b: 'b1', extend: pwlib.extend}; + * obj1.extend({c: 'c1', d: 'd1'}); + * + * // In this case the destination object which is to be extend is + * // obj1. + * + * @param {Boolean} [overwrite=false] If the first argument is a boolean, then + * it will be considered as a boolean flag for overwriting (or not) any existing + * methods and properties in the destination object. Thus, any method and + * property from the source object will take over those in the destination. The + * argument is optional, and if it's omitted, then no method/property will be + * overwritten. + * + * @param {Object} [destination=this] The second argument is the optional + * destination object: the object which will be extended. By default, the + * this object will be extended. + * + * @param {Object} source The third argument must provide list of methods and + * properties which will be added to the destination object. + */ +pwlib.extend = function () { + var name, src, sval, dval; + + if (typeof arguments[0] === 'boolean') { + force = arguments[0]; + dest = arguments[1]; + src = arguments[2]; + } else { + force = false; + dest = arguments[0]; + src = arguments[1]; + } + + if (typeof src === 'undefined') { + src = dest; + dest = this; + } + + if (typeof dest === 'undefined') { + return; + } + + for (name in src) { + sval = src[name]; + dval = dest[name]; + if (force || typeof dval === 'undefined') { + dest[name] = sval; + } + } +}; + +/** + * Retrieve a string formatted with the provided variables. + * + *

The language string must be available in the global lang + * object. + * + *

The string can contain any number of variables in the form of + * {var_name}. + * + * @example + * lang.table_cells = "The table {name} has {n} cells."; + * + * // later ... + * console.log(pwlib.strf(lang.table_cells, {'name' : 'tbl1', 'n' : 11})); + * // The output is 'The table tbl1 has 11 cells.' + * + * @param {String} str The string you want to output. + * + * @param {Object} [vars] The variables you want to set in the language string. + * + * @returns {String} The string updated with the variables you provided. + */ +pwlib.strf = function (str, vars) { + if (!str) { + return str; + } + + var re, i; + + for (i in vars) { + re = new RegExp('{' + i + '}', 'g'); + str = str.replace(re, vars[i]); + } + + return str; +}; + +/** + * Parse a JSON string. This method uses the global JSON parser provided by + * the browser natively. The small difference is that this method allows + * normal JavaScript comments in the JSON string. + * + * @param {String} str The JSON string to parse. + * @returns The JavaScript object that was parsed. + */ +pwlib.jsonParse = function (str) { + str = str.replace(/\s*\/\*(\s|.)+?\*\//g, ''). + replace(/^\s*\/\/.*$/gm, ''); + + return JSON.parse(str); +}; + +/** + * Load a file from a given URL using XMLHttpRequest. + * + * @param {String} url The URL you want to load. + * + * @param {Function} handler The onreadystatechange event handler + * for the XMLHttpRequest object. Your event handler will always receive the + * XMLHttpRequest object as the first parameter. + * + * @param {String} [method="GET"] The HTTP method to use for loading the URL. + * + * @param {String} [send=null] The string you want to send in an HTTP POST + * request. + * + * @param {Object} [headers] An object holding the header names and values you + * want to set for the request. + * + * @returns {XMLHttpRequest} The XMLHttpRequest object created by this method. + * + * @throws {TypeError} If the url is not a string. + */ +pwlib.xhrLoad = function (url, handler, method, send, headers) { + if (typeof url !== 'string') { + throw new TypeError('The first argument must be a string!'); + } + + if (!method) { + method = 'GET'; + } + + if (!headers) { + headers = {}; + } + + if (!send) { + send = null; + } + + /** @ignore */ + var xhr = new XMLHttpRequest(); + /** @ignore */ + xhr.onreadystatechange = function () { handler(xhr); }; + xhr.open(method, url); + + for (var header in headers) { + xhr.setRequestHeader(header, headers[header]); + } + + xhr.send(send); + + return xhr; +}; + +/** + * Check if an URL points to a resource from the same host as the desired one. + * + *

Note that data URIs always return true. + * + * @param {String} url The URL you want to check. + * @param {String} host The host you want in the URL. The host name can include + * the port definition as well. + * + * @returns {Boolean} True if the url points to a resource from the + * host given, or false otherwise. + */ +pwlib.isSameHost = function (url, host) { + if (!url || !host) { + return false; + } + + var pos = url.indexOf(':'), + proto = url.substr(0, pos + 1).toLowerCase(); + + if (proto === 'data:') { + return true; + } + + if (proto !== 'http:' && proto !== 'https:') { + return false; + } + + var urlHost = url.replace(/^https?:\/\//i, ''); + pos = urlHost.indexOf('/'); + if (pos > -1) { + urlHost = urlHost.substr(0, pos); + } + + // remove default port (80) + urlHost = urlHost.replace(/:80$/, ''); + host = host.replace(/:80$/, ''); + + if (!urlHost || !host || urlHost !== host) { + return false; + } + + return true; +}; + +/** + * @class Custom application event. + * + * @param {String} type Event type. + * @param {Boolean} [cancelable=false] Tells if the event can be cancelled or + * not. + * + * @throws {TypeError} If the type parameter is not a string. + * @throws {TypeError} If the cancelable parameter is not a string. + * + * @see pwlib.appEvents for the application events interface which allows adding + * and removing event listeners. + */ +pwlib.appEvent = function (type, cancelable) { + if (typeof type !== 'string') { + throw new TypeError('The first argument must be a string'); + } else if (typeof cancelable === 'undefined') { + cancelable = false; + } else if (typeof cancelable !== 'boolean') { + throw new TypeError('The second argument must be a boolean'); + } + + /** + * Event target object. + * @type Object + */ + this.target = null; + + /** + * Tells if the event can be cancelled or not. + * @type Boolean + */ + this.cancelable = cancelable; + + /** + * Tells if the event has the default action prevented or not. + * @type Boolean + */ + this.defaultPrevented = false; + + /** + * Event type. + * @type String + */ + this.type = type; + + /** + * Prevent the default action of the event. + */ + this.preventDefault = function () { + if (cancelable) { + this.defaultPrevented = true; + } + }; + + /** + * Stop the event propagation to other event handlers. + */ + this.stopPropagation = function () { + this.propagationStopped_ = true; + }; + + this.toString = function () { + return '[pwlib.appEvent.' + this.type + ']'; + }; +}; + +/** + * @class Application initialization event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} state The initialization state. + * @param {String} [errorMessage] The error message, if any. + * + * @throws {TypeError} If the state is not a number. + */ +pwlib.appEvent.appInit = function (state, errorMessage) { + if (typeof state !== 'number') { + throw new TypeError('The first argument must be a number.'); + } + + /** + * Application initialization not started. + * @constant + */ + this.INIT_NOT_STARTED = 0; + + /** + * Application initialization started. + * @constant + */ + this.INIT_STARTED = 1; + + /** + * Application initialization completed successfully. + * @constant + */ + this.INIT_DONE = 2; + + /** + * Application initialization failed. + * @constant + */ + this.INIT_ERROR = -1; + + /** + * Initialization state. + * @type Number + */ + this.state = state; + + /** + * Initialization error message, if any. + * @type String|null + */ + this.errorMessage = errorMessage || null; + + pwlib.appEvent.call(this, 'appInit'); +}; + +/** + * @class Application destroy event. This event is not cancelable. + * + * @augments pwlib.appEvent + */ +pwlib.appEvent.appDestroy = function () { + pwlib.appEvent.call(this, 'appDestroy'); +}; + +/** + * @class GUI show event. This event is not cancelable. + * + * @augments pwlib.appEvent + */ +pwlib.appEvent.guiShow = function () { + pwlib.appEvent.call(this, 'guiShow'); +}; + +/** + * @class GUI hide event. This event is not cancelable. + * + * @augments pwlib.appEvent + */ +pwlib.appEvent.guiHide = function () { + pwlib.appEvent.call(this, 'guiHide'); +}; + +/** + * @class Tool preactivation event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the new tool being activated. + * @param {String|null} prevId The ID of the previous tool. + * + * @throws {TypeError} If the id is not a string. + * @throws {TypeError} If the prevId is not a string or null. + */ +pwlib.appEvent.toolPreactivate = function (id, prevId) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } else if (prevId !== null && typeof prevId !== 'string') { + throw new TypeError('The second argument must be a string or null.'); + } + + /** + * Tool ID. + * @type String + */ + this.id = id; + + /** + * Previous tool ID. + * @type String + */ + this.prevId = prevId; + + pwlib.appEvent.call(this, 'toolPreactivate', true); +}; + +/** + * @class Tool activation event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID the tool which was activated. + * @param {String|null} prevId The ID of the previous tool. + * + * @throws {TypeError} If the id is not a string. + * @throws {TypeError} If the prevId is not a string or null. + */ +pwlib.appEvent.toolActivate = function (id, prevId) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } else if (prevId !== null && typeof prevId !== 'string') { + throw new TypeError('The second argument must be a string or null.'); + } + + /** + * Tool ID. + * @type String + */ + this.id = id; + + /** + * Previous tool ID. + * @type String + */ + this.prevId = prevId; + + pwlib.appEvent.call(this, 'toolActivate'); +}; + +/** + * @class Tool registration event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the tool being registered in an active PaintWeb + * instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.toolRegister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Tool ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'toolRegister'); +}; + +/** + * @class Tool removal event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the tool being unregistered in an active + * PaintWeb instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.toolUnregister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Tool ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'toolUnregister'); +}; + +/** + * @class Extension registration event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the extension being registered in an active + * PaintWeb instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.extensionRegister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Extension ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'extensionRegister'); +}; + +/** + * @class Extension removal event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the extension being unregistered in an active + * PaintWeb instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.extensionUnregister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Extension ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'extensionUnregister'); +}; + +/** + * @class Command registration event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the command being registered in an active + * PaintWeb instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.commandRegister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Command ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'commandRegister'); +}; + +/** + * @class Command removal event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} id The ID of the command being unregistered in an active + * PaintWeb instance. + * + * @throws {TypeError} If the id is not a string. + */ +pwlib.appEvent.commandUnregister = function (id) { + if (typeof id !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + /** + * Command ID. + * @type String + */ + this.id = id; + + pwlib.appEvent.call(this, 'commandUnregister'); +}; + +/** + * @class The image save event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} dataURL The data URL generated by the browser holding the + * pixels of the image being saved, in PNG format. + * @param {Number} width The image width. + * @param {Number} height The image height. + */ +pwlib.appEvent.imageSave = function (dataURL, width, height) { + /** + * The image saved by the browser, using the base64 encoding. + * @type String + */ + this.dataURL = dataURL; + + /** + * Image width. + * @type Number + */ + this.width = width; + + /** + * Image height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'imageSave', true); +}; +/** + * @class The image save result event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Boolean} successful Tells if the image save was successful or not. + * @param {String} [url] The image address. + * @param {String} [urlNew] The new image address. Provide this parameter, if, + * for example, you allow saving images from a remote server to a local server. + * In such cases the image address changes. + */ +pwlib.appEvent.imageSaveResult = function (successful, url, urlNew) { + /** + * Tells if the image save was successful or not. + * @type String + */ + this.successful = successful; + + /** + * The image address. + * @type String|null + */ + this.url = url; + + /** + * The new image address. + * @type String|null + */ + this.urlNew = urlNew; + + pwlib.appEvent.call(this, 'imageSaveResult'); +}; + +/** + * @class History navigation event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} currentPos The new history position. + * @param {Number} previousPos The previous history position. + * @param {Number} states The number of history states available. + * + * @throws {TypeError} If any of the arguments are not numbers. + */ +pwlib.appEvent.historyUpdate = function (currentPos, previousPos, states) { + if (typeof currentPos !== 'number' || typeof previousPos !== 'number' || + typeof states !== 'number') { + throw new TypeError('All arguments must be numbers.'); + } + + /** + * Current history position. + * @type Number + */ + this.currentPos = currentPos; + + /** + * Previous history position. + * @type Number + */ + this.previousPos = previousPos; + + /** + * History states count. + * @type Number + */ + this.states = states; + + pwlib.appEvent.call(this, 'historyUpdate'); +}; + +/** + * @class Image size change event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} width The new image width. + * @param {Number} height The new image height. + * + * @throws {TypeError} If any of the arguments are not numbers. + */ +pwlib.appEvent.imageSizeChange = function (width, height) { + if (typeof width !== 'number' || typeof height !== 'number') { + throw new TypeError('Both arguments must be numbers.'); + } + + /** + * New image width. + * @type Number + */ + this.width = width; + + /** + * New image height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'imageSizeChange'); +}; + +/** + * @class Canvas size change event. This event is not cancelable. + * + *

Note that the Canvas size is not the same as the image size. Canvas size + * refers to the scaling of the Canvas elements being applied (due to image + * zooming or due to browser zoom / DPI). + * + * @augments pwlib.appEvent + * + * @param {Number} width The new Canvas style width. + * @param {Number} height The new Canvas style height. + * @param {Number} scale The new Canvas scaling factor. + * + * @throws {TypeError} If any of the arguments are not numbers. + */ +pwlib.appEvent.canvasSizeChange = function (width, height, scale) { + if (typeof width !== 'number' || typeof height !== 'number' || typeof scale + !== 'number') { + throw new TypeError('All the arguments must be numbers.'); + } + + /** + * New Canvas style width. + * @type Number + */ + this.width = width; + + /** + * New Canvas style height. + * @type Number + */ + this.height = height; + + /** + * The new Canvas scaling factor. + * @type Number + */ + this.scale = scale; + + pwlib.appEvent.call(this, 'canvasSizeChange'); +}; + +/** + * @class Image viewport size change event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} width The new viewport width. This must be a CSS length + * value, like "100px", "100%" or "100em". + * + * @param {String} height The new viewport height. This must be a CSS length + * value, like "100px", "100%" or "100em". + */ +pwlib.appEvent.viewportSizeChange = function (width, height) { + /** + * New viewport width. + * @type String + */ + this.width = width; + + /** + * New viewport height. + * @type String + */ + this.height = height; + + pwlib.appEvent.call(this, 'viewportSizeChange'); +}; + + +/** + * @class Image zoom event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} zoom The new image zoom level. + * + * @throws {TypeError} If the zoom argument is not a number. + */ +pwlib.appEvent.imageZoom = function (zoom) { + if (typeof zoom !== 'number') { + throw new TypeError('The first argument must be a number.'); + } + + /** + * The new image zoom level. + * @type Number + */ + this.zoom = zoom; + + pwlib.appEvent.call(this, 'imageZoom', true); +}; + +/** + * @class Image crop event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} x The crop start position on the x-axis. + * @param {Number} y The crop start position on the y-axis. + * @param {Number} width The cropped image width. + * @param {Number} height The cropped image height. + * + * @throws {TypeError} If any of the arguments are not numbers. + */ +pwlib.appEvent.imageCrop = function (x, y, width, height) { + if (typeof x !== 'number' || typeof y !== 'number' || typeof width !== + 'number' || typeof height !== 'number') { + throw new TypeError('All arguments must be numbers.'); + } + + /** + * The crop start position the x-axis. + * @type Number + */ + this.x = x; + + /** + * The crop start position the y-axis. + * @type Number + */ + this.y = y; + + /** + * The cropped image width. + * @type Number + */ + this.width = width; + + /** + * The cropped image height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'imageCrop', true); +}; + +/** + * @class Configuration change event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {String|Number|Boolean} value The new value. + * @param {String|Number|Boolean} previousValue The previous value. + * @param {String} config The configuration property that just changed. + * @param {String} group The configuration group where the property is found. + * @param {Object} groupRef The configuration group object reference. + * + * @throws {TypeError} If the prop argument is not a string. + * @throws {TypeError} If the group argument is not a string. + * @throws {TypeError} If the groupRef argument is not an object. + */ +pwlib.appEvent.configChange = function (value, previousValue, config, group, + groupRef) { + if (typeof config !== 'string') { + throw new TypeError('The third argument must be a string.'); + } else if (typeof group !== 'string') { + throw new TypeError('The fourth argument must be a string.'); + } else if (typeof groupRef !== 'object') { + throw new TypeError('The fifth argument must be an object.'); + } + + /** + * The new value. + */ + this.value = value; + + /** + * The previous value. + */ + this.previousValue = previousValue; + + /** + * Configuration property name. + * @type String + */ + this.config = config; + + /** + * Configuration group name. + * @type String + */ + this.group = group; + + /** + * Reference to the object holding the configuration property. + * @type Object + */ + this.groupRef = groupRef; + + pwlib.appEvent.call(this, 'configChange'); +}; + +/** + * @class Canvas shadows allowed change event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Boolean} allowed Tells the new allowance value. + * + * @throws {TypeError} If the argument is not a boolean value. + */ +pwlib.appEvent.shadowAllow = function (allowed) { + if (typeof allowed !== 'boolean') { + throw new TypeError('The first argument must be a boolean.'); + } + + /** + * Tells if the Canvas shadows are allowed or not. + * @type Boolean + */ + this.allowed = allowed; + + pwlib.appEvent.call(this, 'shadowAllow'); +}; + +/** + * @class Clipboard update event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {ImageData} data Holds the clipboard ImageData. + */ +pwlib.appEvent.clipboardUpdate = function (data) { + /** + * The clipboard image data. + * @type ImageData + */ + this.data = data; + + pwlib.appEvent.call(this, 'clipboardUpdate'); +}; + +/** + * @class An interface for adding, removing and dispatching of custom + * application events. + * + * @param {Object} target_ The target for all the events. + * + * @see pwlib.appEvent to create application event objects. + */ +pwlib.appEvents = function (target_) { + /** + * Holds the list of event types and event handlers. + * + * @private + * @type Object + */ + var events_ = {}; + + var eventID_ = 1; + + /** + * Add an event listener. + * + * @param {String} type The event you want to listen for. + * @param {Function} handler The event handler. + * + * @returns {Number} The event ID. + * + * @throws {TypeError} If the type argument is not a string. + * @throws {TypeError} If the handler argument is not a function. + * + * @see pwlib.appEvents#remove to remove events. + * @see pwlib.appEvents#dispatch to dispatch an event. + */ + this.add = function (type, handler) { + if (typeof type !== 'string') { + throw new TypeError('The first argument must be a string.'); + } else if (typeof handler !== 'function') { + throw new TypeError('The second argument must be a function.'); + } + + var id = eventID_++; + + if (!(type in events_)) { + events_[type] = {}; + } + + events_[type][id] = handler; + + return id; + }; + + /** + * Remove an event listener. + * + * @param {String} type The event type. + * @param {Number} id The event ID. + * + * @throws {TypeError} If the type argument is not a string. + * + * @see pwlib.appEvents#add to add events. + * @see pwlib.appEvents#dispatch to dispatch an event. + */ + this.remove = function (type, id) { + if (typeof type !== 'string') { + throw new TypeError('The first argument must be a string.'); + } + + if (!(type in events_) || !(id in events_[type])) { + return; + } + + delete events_[type][id]; + }; + + /** + * Dispatch an event. + * + * @param {String} type The event type. + * @param {pwlib.appEvent} ev The event object. + * + * @returns {Boolean} True if the event.preventDefault() has been + * invoked by one of the event handlers, or false if not. + * + * @throws {TypeError} If the type parameter is not a string. + * @throws {TypeError} If the ev parameter is not an object. + * + * @see pwlib.appEvents#add to add events. + * @see pwlib.appEvents#remove to remove events. + * @see pwlib.appEvent the generic event object. + */ + this.dispatch = function (ev) { + if (typeof ev !== 'object') { + throw new TypeError('The second argument must be an object.'); + } else if (typeof ev.type !== 'string') { + throw new TypeError('The second argument must be an application event ' + + 'object.'); + } + + // No event handlers. + if (!(ev.type in events_)) { + return false; + } + + ev.target = target_; + + var id, handlers = events_[ev.type]; + for (id in handlers) { + handlers[id].call(target_, ev); + + if (ev.propagationStopped_) { + break; + } + } + + return ev.defaultPrevented; + }; +}; + + +/** + * @namespace Holds browser information. + */ +pwlib.browser = {}; + +(function () { +var ua = ''; + +if (window.navigator && window.navigator.userAgent) { + ua = window.navigator.userAgent.toLowerCase(); +} + +/** + * @type Boolean + */ +pwlib.browser.opera = window.opera || /\bopera\b/.test(ua); + +/** + * Webkit is the render engine used primarily by Safari. It's also used by + * Google Chrome and GNOME Epiphany. + * + * @type Boolean + */ +pwlib.browser.webkit = !pwlib.browser.opera && + /\b(applewebkit|webkit)\b/.test(ua); + +/** + * Firefox uses the Gecko render engine. + * + * @type Boolean + */ +// In some variations of the User Agent strings provided by Opera, Firefox is +// mentioned. +pwlib.browser.firefox = /\bfirefox\b/.test(ua) && !pwlib.browser.opera; + +/** + * Gecko is the render engine used by Firefox and related products. + * + * @type Boolean + */ +// Typically, the user agent string of WebKit also mentions Gecko. Additionally, +// Opera mentions Gecko for tricking some sites. +pwlib.browser.gecko = /\bgecko\b/.test(ua) && !pwlib.browser.opera && + !pwlib.browser.webkit; + +/** + * Microsoft Internet Explorer. The future of computing. + * + * @type Boolean + */ +// Again, Opera allows users to easily fake the UA. +pwlib.browser.msie = /\bmsie\b/.test(ua) && !pwlib.browser.opera; + +/** + * Presto is the render engine used by Opera. + * + * @type Boolean + */ +// Older versions of Opera did not mention Presto in the UA string. +pwlib.browser.presto = /\bpresto\b/.test(ua) || pwlib.browser.opera; + + +/** + * Browser operating system + * + * @type String + */ +pwlib.browser.os = (ua.match(/\b(windows|linux)\b/) || [])[1]; + +/** + * Tells if the browser is running on an OLPC XO. Typically, only the default + * Gecko-based browser includes the OLPC XO tokens in the user agent string. + * + * @type Boolean + */ +pwlib.browser.olpcxo = /\bolpc\b/.test(ua) && /\bxo\b/.test(ua); + +delete ua; +})(); + + +/** + * @namespace Holds methods and properties necessary for DOM manipulation. + */ +pwlib.dom = {}; + +/** + * @namespace Holds the list of virtual key identifiers and a few characters, + * each being associated to a key code commonly used by Web browsers. + * + * @private + */ +pwlib.dom.keyNames = { + Help: 6, + Backspace: 8, + Tab: 9, + Clear: 12, + Enter: 13, + Shift: 16, + Control: 17, + Alt: 18, + Pause: 19, + CapsLock: 20, + Cancel: 24, + 'Escape': 27, + Space: 32, + PageUp: 33, + PageDown: 34, + End: 35, + Home: 36, + Left: 37, + Up: 38, + Right: 39, + Down: 40, + PrintScreen: 44, + Insert: 45, + 'Delete': 46, + Win: 91, + ContextMenu: 93, + '*': 106, + '+': 107, + F1: 112, + F2: 113, + F3: 114, + F4: 115, + F5: 116, + F6: 117, + F7: 118, + F8: 119, + F9: 120, + F10: 121, + F11: 122, + F12: 123, + NumLock: 144, + ';': 186, + '=': 187, + ',': 188, + '-': 189, + '.': 190, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + "'": 222 +}; + +/** + * @namespace Holds the list of codes, each being associated to a virtual key + * identifier. + * + * @private + */ +pwlib.dom.keyCodes = { + /* + * For almost each key code, these comments give the key name, the + * keyIdentifier from the DOM 3 Events spec and the Unicode character + * information (if you would use the decimal code for direct conversion to + * a character, e.g. String.fromCharCode()). Obviously, the Unicode character + * information is not to be used, since these are only virtual key codes (not + * really char codes) associated to key names. + * + * Each key name in here tries to follow the same style as the defined + * keyIdentifiers from the DOM 3 Events. Thus for the Page Down button, + * 'PageDown' is used (not other variations like 'pag-up'), and so on. + * + * Multiple key codes might be associated to the same key - it's not an error. + * + * Note that this list is not an exhaustive key codes list. This means that + * for key A or for key 0, the script will do String.fromCharCode(keyCode), to + * determine the key. For the case of alpha-numeric keys, this works fine. + */ + + /* + * Key: Enter + * Unicode: U+0003 [End of text] + * + * Note 1: This keyCode is only used in Safari 2 (older Webkit) for the Enter + * key. + * + * Note 2: In Gecko this keyCode is used for the Cancel key (see + * DOM_VK_CANCEL). + */ + 3: 'Enter', + + /* + * Key: Help + * Unicode: U+0006 [Acknowledge] + * + * Note: Taken from Gecko (DOM_VK_HELP). + */ + 6: 'Help', + + /* + * Key: Backspace + * Unicode: U+0008 [Backspace] + * keyIdentifier: U+0008 + */ + 8: 'Backspace', + + /* + * Key: Tab + * Unicode: U+0009 [Horizontal tab] + * keyIdentifier: U+0009 + */ + 9: 'Tab', + + /* + * Key: Enter + * Unicode: U+0010 [Line feed (LF) / New line (NL) / End of line (EOL)] + * + * Note: Taken from the Unicode characters list. If it ends up as a keyCode in + * some event, it's simply considered as being the Enter key. + */ + 10: 'Enter', + + /* + * Key: NumPad_Center + * Unicode: U+000C [Form feed] + * keyIdentifier: Clear + * + * Note 1: This keyCode is used when NumLock is off, and the user pressed the + * 5 key on the numeric pad. + * + * Note 2: Safari 2 (older Webkit) assigns this keyCode to the NumLock key + * itself. + */ + 12: 'Clear', + + /* + * Key: Enter + * Unicode: U+000D [Carriage return (CR)] + * keyIdentifier: Enter + * + * Note 1: This is the keyCode used by most of the Web browsers when the Enter + * key is pressed. + * + * Note 2: Gecko associates the DOM_VK_RETURN to this keyCode. + */ + 13: 'Enter', + + /* + * Key: Enter + * Unicode: U+000E [Shift out] + * + * Note: Taken from Gecko (DOM_VK_ENTER). + */ + 14: 'Enter', + + /* + * Key: Shift + * Unicode: U+0010 [Data link escape] + * keyIdentifier: Shift + * + * Note: In older Safari (Webkit) versions Shift+Tab is assigned a different + * keyCode: keyCode 25. + */ + 16: 'Shift', + + /* + * Key: Control + * Unicode: U+0011 [Device control one] + * keyIdentifier: Control + */ + 17: 'Control', + + /* + * Key: Alt + * Unicode: U+0012 [Device control two] + * keyIdentifier: Alt + */ + 18: 'Alt', + + /* + * Key: Pause + * Unicode: U+0013 [Device control three] + * keyIdentifier: Pause + */ + 19: 'Pause', + + /* + * Key: CapsLock + * Unicode: U+0014 [Device control four] + * keyIdentifier: CapsLock + */ + 20: 'CapsLock', + + /* + * Key: Cancel + * Unicode: U+0018 [Cancel] + * keyIdentifier: U+0018 + */ + 24: 'Cancel', + + /* + * Key: Escape + * Unicode: U+001B [Escape] + * keyIdentifier: U+001B + */ + 27: 'Escape', + + /* + * Key: Space + * Unicode: U+0020 [Space] + * keyIdentifier: U+0020 + */ + 32: 'Space', + + /* + * Key: PageUp or NumPad_North_East + * Unicode: U+0021 ! [Exclamation mark] + * keyIdentifier: PageUp + */ + 33: 'PageUp', + + /* + * Key: PageDown or NumPad_South_East + * Unicode: U+0022 " [Quotation mark] + * keyIdentifier: PageDown + */ + 34: 'PageDown', + + /* + * Key: End or NumPad_South_West + * Unicode: U+0023 # [Number sign] + * keyIdentifier: PageDown + */ + 35: 'End', + + /* + * Key: Home or NumPad_North_West + * Unicode: U+0024 $ [Dollar sign] + * keyIdentifier: Home + */ + 36: 'Home', + + /* + * Key: Left or NumPad_West + * Unicode: U+0025 % [Percent sign] + * keyIdentifier: Left + */ + 37: 'Left', + + /* + * Key: Up or NumPad_North + * Unicode: U+0026 & [Ampersand] + * keyIdentifier: Up + */ + 38: 'Up', + + /* + * Key: Right or NumPad_East + * Unicode: U+0027 ' [Apostrophe] + * keyIdentifier: Right + */ + 39: 'Right', + + /* + * Key: Down or NumPad_South + * Unicode: U+0028 ( [Left parenthesis] + * keyIdentifier: Down + */ + 40: 'Down', + + /* + * Key: PrintScreen + * Unicode: U+002C , [Comma] + * keyIdentifier: PrintScreen + */ + //44: 'PrintScreen', + + /* + * Key: Insert or NumPad_Insert + * Unicode: U+002D - [Hyphen-Minus] + * keyIdentifier: Insert + */ + 45: 'Insert', + + /* + * Key: Delete or NumPad_Delete + * Unicode: U+002E . [Full stop / period] + * keyIdentifier: U+007F + */ + 46: 'Delete', + + /* + * Key: WinLeft + * Unicode: U+005B [ [Left square bracket] + * keyIdentifier: Win + * + * Disabled: rarely needed. + */ + //91: 'Win', + + /* + * Key: WinRight + * Unicode: U+005C \ [Reverse solidus / Backslash] + * keyIdentifier: Win + */ + //92: 'Win', + + /* + * Key: Menu/ContextMenu + * Unicode: U+005D ] [Right square bracket] + * keyIdentifier: ... + * + * Disabled: Is it Meta? Is it Menu, ContextMenu, what? Too much mess. + */ + //93: 'ContextMenu', + + /* + * Key: NumPad_0 + * Unicode: U+0060 ` [Grave accent] + * keyIdentifier: 0 + */ + 96: '0', + + /* + * Key: NumPad_1 + * Unicode: U+0061 a [Latin small letter a] + * keyIdentifier: 1 + */ + 97: '1', + + /* + * Key: NumPad_2 + * Unicode: U+0062 b [Latin small letter b] + * keyIdentifier: 2 + */ + 98: '2', + + /* + * Key: NumPad_3 + * Unicode: U+0063 c [Latin small letter c] + * keyIdentifier: 3 + */ + 99: '3', + + /* + * Key: NumPad_4 + * Unicode: U+0064 d [Latin small letter d] + * keyIdentifier: 4 + */ + 100: '4', + + /* + * Key: NumPad_5 + * Unicode: U+0065 e [Latin small letter e] + * keyIdentifier: 5 + */ + 101: '5', + + /* + * Key: NumPad_6 + * Unicode: U+0066 f [Latin small letter f] + * keyIdentifier: 6 + */ + 102: '6', + + /* + * Key: NumPad_7 + * Unicode: U+0067 g [Latin small letter g] + * keyIdentifier: 7 + */ + 103: '7', + + /* + * Key: NumPad_8 + * Unicode: U+0068 h [Latin small letter h] + * keyIdentifier: 8 + */ + 104: '8', + + /* + * Key: NumPad_9 + * Unicode: U+0069 i [Latin small letter i] + * keyIdentifier: 9 + */ + 105: '9', + + /* + * Key: NumPad_Multiply + * Unicode: U+0070 j [Latin small letter j] + * keyIdentifier: U+002A * [Asterisk / Star] + */ + 106: '*', + + /* + * Key: NumPad_Plus + * Unicode: U+0071 k [Latin small letter k] + * keyIdentifier: U+002B + [Plus] + */ + 107: '+', + + /* + * Key: NumPad_Minus + * Unicode: U+0073 m [Latin small letter m] + * keyIdentifier: U+002D + [Hyphen / Minus] + */ + 109: '-', + + /* + * Key: NumPad_Period + * Unicode: U+0074 n [Latin small letter n] + * keyIdentifier: U+002E . [Period] + */ + 110: '.', + + /* + * Key: NumPad_Division + * Unicode: U+0075 o [Latin small letter o] + * keyIdentifier: U+002F / [Solidus / Slash] + */ + 111: '/', + + 112: 'F1', // p + 113: 'F2', // q + 114: 'F3', // r + 115: 'F4', // s + 116: 'F5', // t + 117: 'F6', // u + 118: 'F7', // v + 119: 'F8', // w + 120: 'F9', // x + 121: 'F10', // y + 122: 'F11', // z + 123: 'F12', // { + + /* + * Key: Delete + * Unicode: U+007F [Delete] + * keyIdentifier: U+007F + */ + 127: 'Delete', + + /* + * Key: NumLock + * Unicode: U+0090 [Device control string] + * keyIdentifier: NumLock + */ + 144: 'NumLock', + + 186: ';', // º (Masculine ordinal indicator) + 187: '=', // » + 188: ',', // ¼ + 189: '-', // ½ + 190: '.', // ¾ + 191: '/', // ¿ + 192: '`', // À + 219: '[', // Û + 220: '\\', // Ü + 221: ']', // Ý + 222: "'" // Þ (Latin capital letter thorn) + + //224: 'Win', // à + //229: 'WinIME', // å or WinIME or something else in Webkit + //255: 'NumLock', // ÿ, Gecko and Chrome, Windows XP in VirtualBox + //376: 'NumLock' // Ÿ, Opera, Windows XP in VirtualBox +}; + +if (pwlib.browser.gecko) { + pwlib.dom.keyCodes[3] = 'Cancel'; // DOM_VK_CANCEL +} + +/** + * @namespace Holds a list of common wrong key codes in Web browsers. + * + * @private + */ +pwlib.dom.keyCodes_fixes = { + 42: pwlib.dom.keyNames['*'], // char * to key * + 47: pwlib.dom.keyNames['/'], // char / to key / + 59: pwlib.dom.keyNames[';'], // char ; to key ; + 61: pwlib.dom.keyNames['='], // char = to key = + 96: 48, // NumPad_0 to char 0 + 97: 49, // NumPad_1 to char 1 + 98: 50, // NumPad_2 to char 2 + 99: 51, // NumPad_3 to char 3 + 100: 52, // NumPad_4 to char 4 + 101: 53, // NumPad_5 to char 5 + 102: 54, // NumPad_6 to char 6 + 103: 55, // NumPad_7 to char 7 + 104: 56, // NumPad_8 to char 8 + 105: 57, // NumPad_9 to char 9 + //106: 56, // NumPad_Multiply to char 8 + //107: 187, // NumPad_Plus to key = + 109: pwlib.dom.keyNames['-'], // NumPad_Minus to key - + 110: pwlib.dom.keyNames['.'], // NumPad_Period to key . + 111: pwlib.dom.keyNames['/'] // NumPad_Division to key / +}; + +/** + * @namespace Holds the list of broken key codes generated by older Webkit + * (Safari 2). + * + * @private + */ +pwlib.dom.keyCodes_Safari2 = { + 63232: pwlib.dom.keyNames.Up, // 38 + 63233: pwlib.dom.keyNames.Down, // 40 + 63234: pwlib.dom.keyNames.Left, // 37 + 63235: pwlib.dom.keyNames.Right, // 39 + 63236: pwlib.dom.keyNames.F1, // 112 + 63237: pwlib.dom.keyNames.F2, // 113 + 63238: pwlib.dom.keyNames.F3, // 114 + 63239: pwlib.dom.keyNames.F4, // 115 + 63240: pwlib.dom.keyNames.F5, // 116 + 63241: pwlib.dom.keyNames.F6, // 117 + 63242: pwlib.dom.keyNames.F7, // 118 + 63243: pwlib.dom.keyNames.F8, // 119 + 63244: pwlib.dom.keyNames.F9, // 120 + 63245: pwlib.dom.keyNames.F10, // 121 + 63246: pwlib.dom.keyNames.F11, // 122 + 63247: pwlib.dom.keyNames.F12, // 123 + 63248: pwlib.dom.keyNames.PrintScreen, // 44 + 63272: pwlib.dom.keyNames['Delete'], // 46 + 63273: pwlib.dom.keyNames.Home, // 36 + 63275: pwlib.dom.keyNames.End, // 35 + 63276: pwlib.dom.keyNames.PageUp, // 33 + 63277: pwlib.dom.keyNames.PageDown, // 34 + 63289: pwlib.dom.keyNames.NumLock, // 144 + 63302: pwlib.dom.keyNames.Insert // 45 +}; + + +/** + * A complete keyboard events cross-browser compatibility layer. + * + *

Unfortunately, due to the important differences across Web browsers, + * simply using the available properties in a single keyboard event is not + * enough to accurately determine the key the user pressed. Thus, one needs to + * have event handlers for all keyboard-related events keydown, + * keypress and keyup. + * + *

This class provides a complete keyboard event compatibility layer. For any + * new instance you provide the DOM element you want to listen events for, and + * the event handlers for any of the three events keydown + * / keypress / keyup. + * + *

Your event handlers will receive the original DOM Event object, with + * several new properties defined: + * + *

+ * + *

The character-related properties, charCode_ and + * char_ are only available in the keypress and + * keyup event objects. + * + *

This class will ensure that the keypress event is always + * fired in Webkit and MSIE for all keys, except modifiers. For modifier keys + * like Shift, Control, and Alt, the + * keypress event will not be fired, even if the Web browser does + * it. + * + *

Some user agents like Webkit repeat the keydown event while + * the user holds down a key. This class will ensure that only the + * keypress event is repeated. + * + *

If you want to prevent the default action for an event, you should prevent + * it on keypress. This class will prevent the default action for + * keydown if need (in MSIE). + * + * @example + * var klogger = function (ev) { + * console.log(ev.type + + * ' keyCode_ ' + ev.keyCode_ + + * ' key_ ' + ev.key_ + + * ' charCode_ ' + ev.charCode_ + + * ' char_ ' + ev.char_ + + * ' repeat_ ' + ev.repeat_); + * }; + * + * var kbListener = new pwlib.dom.KeyboardEventListener(window, + * {keydown: klogger, + * keypress: klogger, + * keyup: klogger}); + * + * // later when you're done... + * kbListener.detach(); + * + * @class A complete keyboard events cross-browser compatibility layer. + * + * @param {Element} elem_ The DOM Element you want to listen events for. + * + * @param {Object} handlers_ The object holding the list of event handlers + * associated to the name of each keyboard event you want to listen. To listen + * for all the three keyboard events use {keydown: fn1, + * keypress: fn2, keyup: fn3}. + * + * @throws {TypeError} If the handlers_ object does not contain any + * event handler. + */ +pwlib.dom.KeyboardEventListener = function (elem_, handlers_) { + /* + Technical details: + + For the keyup and keydown events the keyCode provided is that of the virtual + key irrespective of other modifiers (e.g. Shift). Generally, during the + keypress event, the keyCode holds the Unicode value of the character + resulted from the key press, say an alphabetic upper/lower-case char, + depending on the actual intent of the user and depending on the currently + active keyboard layout. + + Examples: + * Pressing p you get keyCode 80 in keyup/keydown, and keyCode 112 in + keypress. String.fromCharCode(80) = 'P' and String.fromCharCode(112) = 'p'. + * Pressing P you get keyCode 80 in all events. + * Pressing F1 you get keyCode 112 in keyup, keydown and keypress. + * Pressing 9 you get keyCode 57 in all events. + * Pressing Shift+9 you get keyCode 57 in keyup/keydown, and keyCode 40 in + keypress. String.fromCharCode(57) = '9' and String.fromCharCode(40) = '('. + + * Using the Greek layout when you press v on an US keyboard you get the + output character ω. The keyup/keydown events hold keyCode 86 which is V. + This does make sense, since it's the virtual key code we are dealing with + - not the character code, not the result of pressing the key. The keypress + event will hold keyCode 969 (ω). + + * Pressing NumPad_Minus you get keyCode 109 in keyup/keydown and keyCode 45 + in keypress. Again, this happens because in keyup/keydown you don't get the + character code, you get the key code, as indicated above. For + your information: String.fromCharCode(109) = 'm' and String.fromCharCode(45) + = '-'. + + Therefore, we need to map all the codes of several keys, like F1-F12, + Escape, Enter, Tab, etc. This map is held by pwlib.dom.keyCodes. It + associates, for example, code 112 to F1, or 13 to Enter. This map is used to + detect virtual keys in all events. + + (This is only the general story, details about browser-specific differences + follow below.) + + If the code given by the browser doesn't appear in keyCode maps, it's used + as is. The key_ returned is that of String.fromCharCode(keyCode). + + In all browsers we consider all events having keyCode <= 32, as being events + generated by a virtual key (not a character). As such, the keyCode value is + always searched in pwlib.dom.keyCodes. + + As you might notice from the above description, in the keypress event we + cannot tell the difference from say F1 and p, because both give the code + 112. In Gecko and Webkit we can tell the difference because these UAs also + set the charCode event property when the key generates a character. If F1 is + pressed, or some other virtual key, charCode is never set. + + In Opera the charCode property is never set. However, the 'which' event + property is not set for several virtual keys. This means we can tell the + difference between a character and a virtual key. However, there's a catch: + not *all* virtual keys have the 'which' property unset. Known exceptions: + Backspace (8), Tab (9), Enter (13), Shift (16), Control (17), Alt (18), + Pause (19), Escape (27), End (35), Home (36), Insert (45), Delete (46) and + NumLock (144). Given we already consider any keyCode <= 32 being one of some + virtual key, fewer exceptions remain. We only have the End, Home, Insert, + Delete and the NumLock keys which cannot be 100% properly detected in the + keypress event, in Opera. To properly detect End/Home we can check if the + Shift modifier is active or not. If the user wants # instead of End, then + Shift must be active. The same goes for $ and Home. Thus we now only have + the '-' (Insert) and the '.' (Delete) characters incorrectly detected as + being Insert/Delete. + + The above brings us to one of the main visible difference, when comparing + the pwlib.dom.KeyboardEventListener class and the simple + pwlib.dom.KeyboardEvent.getKey() function. In getKey(), for the keypress + event we cannot accurately determine the exact key, because it requires + checking the keyCode used for the keydown event. The KeyboardEventListener + class monitors all the keyboard events, ensuring a more accurate key + detection. + + Different keyboard layouts and international characters are generally + supported. Tested and known to work with the Cyrillic alphabet (Greek + keyboard layout) and with the US Dvorak keyboard layouts. + + Opera does not fire the keyup event for international characters when + running on Linux. For example, this happens with the Greek keyboard layout, + when trying Cyrillic characters. + + Gecko gives no keyCode/charCode/which for international characters when + running on Linux, in the keyup/keydown events. Thus, all such keys remain + unidentified for these two events. For the keypress event there are no + issues with such characters. + + Webkit and Konqueror 4 also implement the keyIdentifier property from the + DOM 3 Events specification. In theory, this should be great, but it's not + without problems. Sometimes keyCode/charCode/which are all 0, but + keyIdentifier is properly set. For several virtual keys the keyIdentifier + value is simply 'U+0000'. Thus, the keyIdentifier is used only if the value + is not 'Unidentified' / 'U+0000', and only when keyCode/charCode/which are + not available. + + Konqueror 4 does not use the 'U+XXXX' notation for Unicode characters. It + simply gives the character, directly. + + Additionally, Konqueror seems to have some problems with several keyCodes in + keydown/keyup. For example, the key '[' gives keyCode 91 instead of 219. + Thus, it effectively gives the Unicode for the character, not the key code. + This issue is visible with other key as well. + + NumPad_Clear is unidentified on Linux in all browsers, but it works on + Windows. + + In MSIE the keypress event is only fired for characters and for Escape, + Space and Enter. Similarly, Webkit only fires the keypress event for + characters. However, Webkit does not fire keypress for Escape. + + International characters and different keyboard layouts seem to work fine in + MSIE as well. + + As of MSIE 4.0, the keypress event fires for the following keys: + * Letters: A - Z (uppercase and lowercase) + * Numerals: 0 - 9 + * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~ + * System: Escape (27), Space (32), Enter (13) + + Documentation about the keypress event: + http://msdn.microsoft.com/en-us/library/ms536939(VS.85).aspx + + As of MSIE 4.0, the keydown event fires for the following keys: + * Editing: Delete (46), Insert (45) + * Function: F1 - F12 + * Letters: A - Z (uppercase and lowercase) + * Navigation: Home, End, Left, Right, Up, Down + * Numerals: 0 - 9 + * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~ + * System: Escape (27), Space (32), Shift (16), Tab (9) + + As of MSIE 5, the event also fires for the following keys: + * Editing: Backspace (8) + * Navigation: PageUp (33), PageDown (34) + * System: Shift+Tab (9) + + Documentation about the keydown event: + http://msdn.microsoft.com/en-us/library/ms536938(VS.85).aspx + + As of MSIE 4.0, the keyup event fires for the following keys: + * Editing: Delete, Insert + * Function: F1 - F12 + * Letters: A - Z (uppercase and lowercase) + * Navigation: Home (36), End (35), Left (37), Right (39), Up (38), Down (40) + * Numerals: 0 - 9 + * Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~ + * System: Escape (27), Space (32), Shift (16), Tab (9) + + As of MSIE 5, the event also fires for the following keys: + * Editing: Backspace (8) + * Navigation: PageUp (33), PageDown (34) + * System: Shift+Tab (9) + + Documentation about the keyup event: + http://msdn.microsoft.com/en-us/library/ms536940(VS.85).aspx + + For further gory details and a different implementation see: + http://code.google.com/p/doctype/source/browse/trunk/goog/events/keycodes.js + http://code.google.com/p/doctype/source/browse/trunk/goog/events/keyhandler.js + + Opera keydown/keyup: + These events fire for all keys, including for modifiers. + keyCode is always set. + charCode is never set. + which is always set. + keyIdentifier is always undefined. + + Opera keypress: + This event fires for all keys, except for modifiers themselves. + keyCode is always set. + charCode is never set. + which is set for all characters. which = 0 for several virtual keys. + which is known to be set for: Backspace (8), Tab (9), Enter (13), Shift + (16), Control (17), Alt (18), Pause (19), Escape (27), End (35), Home + (36), Insert (45), Delete (46), NumLock (144). + which is known to be unset for: F1 - F12, PageUp (33), PageDown (34), Left + (37), Up (38), Right (39), Down (40). + keyIdentifier is always undefined. + + MSIE keyup/keypress/keydown: + Event firing conditions are described above. + keyCode is always set. + charCode is never set. + which is never set. + keyIdentifier is always undefined. + + Webkit keydown/keyup: + These events fires for all keys, including for modifiers. + keyCode is always set. + charCode is never set. + which is always set. + keyIdentifier is always set. + + Webkit keypress: + This event fires for characters keys, similarly to MSIE (see above info). + keyCode is always set. + charCode is always set for all characters. + which is always set. + keyIdentifier is null. + + Gecko keydown/keyup: + These events fire for all keys, including for modifiers. + keyCode is always set. + charCode is never set. + which is always set. + keyIdentifier is always undefined. + + Gecko keypress: + This event fires for all keys, except for modifiers themselves. + keyCode is only set for virtual keys, not for characters. + charCode is always set for all characters. + which is always set for all characters and for the Enter virtual key. + keyIdentifier is always undefined. + + Another important difference between the KeyboardEventListener class and the + getKey() function is that the class tries to ensure that the keypress event + is fired for the handler, even if the Web browser does not do it natively. + Also, the class tries to provide a consistent approach to keyboard event + repetition when the user holds down a key for longer periods of time, by + repeating only the keypress event. + + On Linux, Opera, Firefox and Konqueror do not repeat the keydown event, only + keypress. On Windows, Opera, Firefox and MSIE do repeat the keydown and + keypress events while the user holds down the key. Webkit repeats the + keydown and the keypress (when it fires) events on both systems. + + The default action can be prevented for during keydown in MSIE, and during + keypress for the other browsers. In Webkit when keypress doesn't fire, + keydown needs to be prevented. + + The KeyboardEventListener class tries to bring consistency. The keydown + event never repeats, only the keypress event repeats and it always fires for + all keys. The keypress event never fires for modifiers. Events should always + be prevented during keypress - the class deals with preventing the event + during keydown or keypress as needed in Webkit and MSIE. + + If no code/keyIdentifier is given by the browser, the getKey() function + returns null. In the case of the KeyboardEventListener class, keyCode_ + / key_ / charCode_ / char_ will be null or undefined. + */ + + /** + * During a keyboard event flow, this holds the current key code, starting + * from the keydown event. + * + * @private + * @type Number + */ + var keyCode_ = null; + + /** + * During a keyboard event flow, this holds the current key, starting from the + * keydown event. + * + * @private + * @type String + */ + var key_ = null; + + /** + * During a keyboard event flow, this holds the current character code, + * starting from the keypress event. + * + * @private + * @type Number + */ + var charCode_ = null; + + /** + * During a keyboard event flow, this holds the current character, starting + * from the keypress event. + * + * @private + * @type String + */ + var char_ = null; + + /** + * True if the current keyboard event is repeating. This happens when the user + * holds down a key for longer periods of time. + * + * @private + * @type Boolean + */ + var repeat_ = false; + + + if (!handlers_) { + throw new TypeError('The first argument must be of type an object.'); + } + + if (!handlers_.keydown && !handlers_.keypress && !handlers_.keyup) { + throw new TypeError('The provided handlers object has no keyboard event' + + 'handler.'); + } + + if (handlers_.keydown && typeof handlers_.keydown !== 'function') { + throw new TypeError('The keydown event handler is not a function!'); + } + if (handlers_.keypress && typeof handlers_.keypress !== 'function') { + throw new TypeError('The keypress event handler is not a function!'); + } + if (handlers_.keyup && typeof handlers_.keyup !== 'function') { + throw new TypeError('The keyup event handler is not a function!'); + } + + /** + * Attach the keyboard event listeners to the current DOM element. + */ + this.attach = function () { + keyCode_ = null; + key_ = null; + charCode_ = null; + char_ = null; + repeat_ = false; + + // FIXME: I have some ideas for a solution to the problem of having multiple + // event handlers like these attached to the same element. Somehow, only one + // should do all the needed work. + + elem_.addEventListener('keydown', keydown, false); + elem_.addEventListener('keypress', keypress, false); + elem_.addEventListener('keyup', keyup, false); + }; + + /** + * Detach the keyboard event listeners from the current DOM element. + */ + this.detach = function () { + elem_.removeEventListener('keydown', keydown, false); + elem_.removeEventListener('keypress', keypress, false); + elem_.removeEventListener('keyup', keyup, false); + + keyCode_ = null; + key_ = null; + charCode_ = null; + char_ = null; + repeat_ = false; + }; + + /** + * Dispatch an event. + * + *

This function simply invokes the handler for the event of the given + * type. The handler receives the ev event. + * + * @private + * @param {String} type The event type to dispatch. + * @param {Event} ev The DOM Event object to dispatch to the handler. + */ + function dispatch (type, ev) { + if (!handlers_[type]) { + return; + } + + var handler = handlers_[type]; + + if (type === ev.type) { + handler.call(elem_, ev); + + } else { + // This happens when the keydown event tries to dispatch a keypress event. + + // FIXME: I could use createEvent() ... food for thought for later. + + /** @ignore */ + var ev_new = {}; + pwlib.extend(ev_new, ev); + ev_new.type = type; + + // Make sure preventDefault() is not borked... + /** @ignore */ + ev_new.preventDefault = function () { + ev.preventDefault(); + }; + + handler.call(elem_, ev_new); + } + }; + + /** + * The keydown event handler. This function determines the key + * pressed by the user, and checks if the keypress event will + * fire in the current Web browser, or not. If it does not, a synthetic + * keypress event will be fired. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function keydown (ev) { + var prevKey = key_; + + charCode_ = null; + char_ = null; + + findKeyCode(ev); + + ev.keyCode_ = keyCode_; + ev.key_ = key_; + ev.repeat_ = key_ && prevKey === key_ ? true : false; + + repeat_ = ev.repeat_; + + // When the user holds down a key for a longer period of time, the keypress + // event is generally repeated. However, in Webkit keydown is repeated (and + // keypress if it fires keypress for the key). As such, we do not dispatch + // the keydown event when a key event starts to be repeated. + if (!repeat_) { + dispatch('keydown', ev); + } + + // MSIE and Webkit only fire the keypress event for characters + // (alpha-numeric and symbols). + if (!isModifierKey(key_) && !firesKeyPress(ev)) { + ev.type_ = 'keydown'; + keypress(ev); + } + }; + + /** + * The keypress event handler. This function determines the + * character generated by the keyboard event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function keypress (ev) { + // We reuse the keyCode_/key_ from the keydown event, because ev.keyCode + // generally holds the character code during the keypress event. + // However, if keyCode_ is not available, try to determine the key for this + // event as well. + if (!keyCode_) { + findKeyCode(ev); + repeat_ = false; + } + + ev.keyCode_ = keyCode_; + ev.key_ = key_; + + findCharCode(ev); + + ev.charCode_ = charCode_; + ev.char_ = char_; + + // Any subsequent keypress event is considered a repeated keypress (the user + // is holding down the key). + ev.repeat_ = repeat_; + if (!repeat_) { + repeat_ = true; + } + + if (!isModifierKey(key_)) { + dispatch('keypress', ev); + } + }; + + /** + * The keyup event handler. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function keyup (ev) { + /* + * Try to determine the keyCode_ for keyup again, even if we might already + * have it from keydown. This is needed because the user might press some + * key which only generates the keydown and keypress events, after which + * a sudden keyup event is fired for a completely different key. + * + * Example: in Opera press F2 then Escape. It will first generate two + * events, keydown and keypress, for the F2 key. When you press Escape to + * close the dialog box, the script receives keyup for Escape. + */ + findKeyCode(ev); + + ev.keyCode_ = keyCode_; + ev.key_ = key_; + + // Provide the character info from the keypress event in keyup as well. + ev.charCode_ = charCode_; + ev.char_ = char_; + + dispatch('keyup', ev); + + keyCode_ = null; + key_ = null; + charCode_ = null; + char_ = null; + repeat_ = false; + }; + + /** + * Tells if the key is a modifier or not. + * + * @private + * @param {String} key The key name. + * @returns {Boolean} True if the key is a modifier, or false if + * not. + */ + function isModifierKey (key) { + switch (key) { + case 'Shift': + case 'Control': + case 'Alt': + case 'Meta': + case 'Win': + return true; + default: + return false; + } + }; + + /** + * Tells if the current Web browser will fire the keypress event + * for the current keydown event object. + * + * @private + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the Web browser will fire + * a keypress event, or false if not. + */ + function firesKeyPress (ev) { + // Gecko does not fire keypress for the Up/Down arrows when the target is an + // input element. + if ((key_ === 'Up' || key_ === 'Down') && pwlib.browser.gecko && ev.target + && ev.target.tagName.toLowerCase() === 'input') { + return false; + } + + if (!pwlib.browser.msie && !pwlib.browser.webkit) { + return true; + } + + // Check if the key is a character key, or not. + // If it's not a character, then keypress will not fire. + // Known exceptions: keypress fires for Space, Enter and Escape in MSIE. + if (key_ && key_ !== 'Space' && key_ !== 'Enter' && key_ !== 'Escape' && + key_.length !== 1) { + return false; + } + + // Webkit doesn't fire keypress for Escape as well ... + if (pwlib.browser.webkit && key_ === 'Escape') { + return false; + } + + // MSIE does not fire keypress if you hold Control / Alt down, while Shift + // is off. Albeit, based on testing I am not completely sure if Shift needs + // to be down or not. Sometimes MSIE won't fire keypress even if I hold + // Shift down, and sometimes it does. Eh. + if (pwlib.browser.msie && !ev.shiftKey && (ev.ctrlKey || ev.altKey)) { + return false; + } + + return true; + }; + + /** + * Determine the key and the key code for the current DOM Event object. This + * function updates the keyCode_ and the key_ variables + * to hold the result. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function findKeyCode (ev) { + /* + * If the event has no keyCode/which/keyIdentifier values, then simply do + * not overwrite any existing keyCode_/key_. + */ + if (ev.type === 'keyup' && !ev.keyCode && !ev.which && (!ev.keyIdentifier || + ev.keyIdentifier === 'Unidentified' || ev.keyIdentifier === 'U+0000')) { + return; + } + + keyCode_ = null; + key_ = null; + + // Try to use keyCode/which. + if (ev.keyCode || ev.which) { + keyCode_ = ev.keyCode || ev.which; + + // Fix Webkit quirks + if (pwlib.browser.webkit) { + // Old Webkit gives keyCode 25 when Shift+Tab is used. + if (keyCode_ == 25 && this.shiftKey) { + keyCode_ = pwlib.dom.keyNames.Tab; + } else if (keyCode_ >= 63232 && keyCode_ in pwlib.dom.keyCodes_Safari2) { + // Old Webkit gives wrong values for several keys. + keyCode_ = pwlib.dom.keyCodes_Safari2[keyCode_]; + } + } + + // Fix keyCode quirks in all browsers. + if (keyCode_ in pwlib.dom.keyCodes_fixes) { + keyCode_ = pwlib.dom.keyCodes_fixes[keyCode_]; + } + + key_ = pwlib.dom.keyCodes[keyCode_] || String.fromCharCode(keyCode_); + + return; + } + + // Try to use ev.keyIdentifier. This is only available in Webkit and + // Konqueror 4, each having some quirks. Sometimes the property is needed, + // because keyCode/which are not always available. + + var key = null, + keyCode = null, + id = ev.keyIdentifier; + + if (!id || id === 'Unidentified' || id === 'U+0000') { + return; + } + + if (id.substr(0, 2) === 'U+') { + // Webkit gives character codes using the 'U+XXXX' notation, as per spec. + keyCode = parseInt(id.substr(2), 16); + + } else if (id.length === 1) { + // Konqueror 4 implements keyIdentifier, and they provide the Unicode + // character directly, instead of using the 'U+XXXX' notation. + keyCode = id.charCodeAt(0); + key = id; + + } else { + /* + * Common keyIdentifiers like 'PageDown' are used as they are. + * We determine the common keyCode used by Web browsers, from the + * pwlib.dom.keyNames object. + */ + keyCode_ = pwlib.dom.keyNames[id] || null; + key_ = id; + + return; + } + + // Some keyIdentifiers like 'U+007F' (127: Delete) need to become key names. + if (keyCode in pwlib.dom.keyCodes && (keyCode <= 32 || keyCode == 127 || + keyCode == 144)) { + key_ = pwlib.dom.keyCodes[keyCode]; + } else { + if (!key) { + key = String.fromCharCode(keyCode); + } + + // Konqueror gives lower-case chars + key_ = key.toUpperCase(); + if (key !== key_) { + keyCode = key_.charCodeAt(0); + } + } + + // Correct the keyCode, make sure it's a common keyCode, not the Unicode + // decimal representation of the character. + if (key_ === 'Delete' || key_.length === 1 && key_ in pwlib.dom.keyNames) { + keyCode = pwlib.dom.keyNames[key_]; + } + + keyCode_ = keyCode; + }; + + /** + * Determine the character and the character code for the current DOM Event + * object. This function updates the charCode_ and the + * char_ variables to hold the result. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function findCharCode (ev) { + charCode_ = null; + char_ = null; + + // Webkit and Gecko implement ev.charCode. + if (ev.charCode) { + charCode_ = ev.charCode; + char_ = String.fromCharCode(ev.charCode); + + return; + } + + // Try the keyCode mess. + if (ev.keyCode || ev.which) { + var keyCode = ev.keyCode || ev.which; + + var force = false; + + // We accept some keyCodes. + switch (keyCode) { + case pwlib.dom.keyNames.Tab: + case pwlib.dom.keyNames.Enter: + case pwlib.dom.keyNames.Space: + force = true; + } + + // Do not consider the keyCode a character code, if during the keydown + // event it was determined the key does not generate a character, unless + // it's Tab, Enter or Space. + if (!force && key_ && key_.length !== 1) { + return; + } + + // If the keypress event at hand is synthetically dispatched by keydown, + // then special treatment is needed. This happens only in Webkit and MSIE. + if (ev.type_ === 'keydown') { + var key = pwlib.dom.keyCodes[keyCode]; + // Check if the keyCode points to a single character. + // If it does, use it. + if (key && key.length === 1) { + charCode_ = key.charCodeAt(0); // keyCodes != charCodes + char_ = key; + } + } else if (keyCode >= 32 || force) { + // For normal keypress events, we are done. + charCode_ = keyCode; + char_ = String.fromCharCode(keyCode); + } + + if (charCode_) { + return; + } + } + + /* + * Webkit and Konqueror do not provide a keyIdentifier in the keypress + * event, as per spec. However, in the unlikely case when the keyCode is + * missing, and the keyIdentifier is available, we use it. + * + * This property might be used when a synthetic keypress event is generated + * by the keydown event, and keyCode/charCode/which are all not available. + */ + + var c = null, + charCode = null, + id = ev.keyIdentifier; + + if (id && id !== 'Unidentified' && id !== 'U+0000' && + (id.substr(0, 2) === 'U+' || id.length === 1)) { + + // Characters in Konqueror... + if (id.length === 1) { + charCode = id.charCodeAt(0); + c = id; + + } else { + // Webkit uses the 'U+XXXX' notation as per spec. + charCode = parseInt(id.substr(2), 16); + } + + if (charCode == pwlib.dom.keyNames.Tab || + charCode == pwlib.dom.keyNames.Enter || + charCode >= 32 && charCode != 127 && + charCode != pwlib.dom.keyNames.NumLock) { + + charCode_ = charCode; + char_ = c || String.fromCharCode(charCode); + + return; + } + } + + // Try to use the key determined from the previous keydown event, if it + // holds a character. + if (key_ && key_.length === 1) { + charCode_ = key_.charCodeAt(0); + char_ = key_; + } + }; + + this.attach(); +}; + +// Check out the libmacrame project: http://code.google.com/p/libmacrame. + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/bcurve.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/bcurve.png new file mode 100644 index 00000000..318bf8da Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/bcurve.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cbucket.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cbucket.png new file mode 100644 index 00000000..c2aa20d1 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cbucket.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/clipboardPaste.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/clipboardPaste.png new file mode 100644 index 00000000..826a4038 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/clipboardPaste.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cpicker.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cpicker.png new file mode 100644 index 00000000..6cceff6f Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/cpicker.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/ellipse.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/ellipse.png new file mode 100644 index 00000000..8975f7fc Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/ellipse.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/eraser.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/eraser.png new file mode 100644 index 00000000..413a8457 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/eraser.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/hand.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/hand.png new file mode 100644 index 00000000..d4612127 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/hand.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/help.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/help.png new file mode 100644 index 00000000..b19034ec Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/help.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyRedo.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyRedo.png new file mode 100644 index 00000000..78069520 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyRedo.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyUndo.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyUndo.png new file mode 100644 index 00000000..2f6f12e9 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/historyUndo.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_addPhoto.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_addPhoto.png new file mode 100644 index 00000000..2d0171df Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_addPhoto.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_block.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_block.png new file mode 100644 index 00000000..3604c0c2 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_block.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cancel.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cancel.png new file mode 100644 index 00000000..061585c1 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cancel.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_centerAlign.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_centerAlign.png new file mode 100644 index 00000000..e3eb30ca Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_centerAlign.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_colorfill.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_colorfill.png new file mode 100644 index 00000000..c42d0245 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_colorfill.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_copy.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_copy.png new file mode 100644 index 00000000..c7330ce9 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_copy.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_crop.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_crop.png new file mode 100644 index 00000000..a715dcad Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_crop.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cut.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cut.png new file mode 100644 index 00000000..8b1bf0a2 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_cut.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_delete.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_delete.png new file mode 100644 index 00000000..f38fcc99 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_delete.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontBold.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontBold.png new file mode 100644 index 00000000..bb83fb87 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontBold.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontItalic.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontItalic.png new file mode 100644 index 00000000..d5f72f23 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontItalic.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontSize.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontSize.png new file mode 100644 index 00000000..21144948 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_fontSize.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_leftAlign.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_leftAlign.png new file mode 100644 index 00000000..66a53ef9 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_leftAlign.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_paste.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_paste.png new file mode 100644 index 00000000..dbf17412 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_paste.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_pinch.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_pinch.png new file mode 100644 index 00000000..4f01a950 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_pinch.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_replay.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_replay.png new file mode 100644 index 00000000..68147b69 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_replay.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rightAlign.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rightAlign.png new file mode 100644 index 00000000..f91bdea5 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rightAlign.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rotation.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rotation.png new file mode 100644 index 00000000..4cff2e9b Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_rotation.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_save.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_save.png new file mode 100644 index 00000000..752d71c4 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_save.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_select.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_select.png new file mode 100644 index 00000000..83067d2e Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_select.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_square.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_square.png new file mode 100644 index 00000000..107393d2 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/i_square.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageClear.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageClear.png new file mode 100644 index 00000000..26d384d3 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageClear.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageRotate.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageRotate.png new file mode 100644 index 00000000..c283211f Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageRotate.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageSave.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageSave.png new file mode 100644 index 00000000..d21304a8 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/imageSave.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/insertimg.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/insertimg.png new file mode 100644 index 00000000..bddd152c Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/insertimg.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-butt.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-butt.png new file mode 100644 index 00000000..02ea7bc7 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-butt.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-round.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-round.png new file mode 100644 index 00000000..2db018b8 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-round.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-square.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-square.png new file mode 100644 index 00000000..9eb3e966 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-cap-square.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-bevel.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-bevel.png new file mode 100644 index 00000000..e174aea1 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-bevel.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-miter.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-miter.png new file mode 100644 index 00000000..bec853be Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-miter.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-round.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-round.png new file mode 100644 index 00000000..0284e1b5 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line-join-round.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line.png new file mode 100644 index 00000000..75903a75 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/line.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/paintweb.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/paintweb.png new file mode 100644 index 00000000..8264f219 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/paintweb.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/pencil.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/pencil.png new file mode 100644 index 00000000..1a4e4a09 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/pencil.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/polygon.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/polygon.png new file mode 100644 index 00000000..effe5c65 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/polygon.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/rectangle.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/rectangle.png new file mode 100644 index 00000000..8026d87f Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/rectangle.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selection.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selection.png new file mode 100644 index 00000000..6adf333a Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selection.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCopy.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCopy.png new file mode 100644 index 00000000..7f685865 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCopy.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCrop.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCrop.png new file mode 100644 index 00000000..3888c3d2 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCrop.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCut.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCut.png new file mode 100644 index 00000000..aa44955c Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionCut.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionDelete.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionDelete.png new file mode 100644 index 00000000..2ee02ce2 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionDelete.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionFill.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionFill.png new file mode 100644 index 00000000..c98ed306 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/selectionFill.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-both.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-both.png new file mode 100644 index 00000000..64109c10 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-both.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-fill.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-fill.png new file mode 100644 index 00000000..798cd8c7 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-fill.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-stroke.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-stroke.png new file mode 100644 index 00000000..59fde256 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/shapeType-stroke.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-center.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-center.png new file mode 100644 index 00000000..ac78087c Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-center.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-left.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-left.png new file mode 100644 index 00000000..0ba1194c Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-left.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-right.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-right.png new file mode 100644 index 00000000..b263fa64 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-align-right.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-bold.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-bold.png new file mode 100644 index 00000000..27f8642c Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-bold.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-italic.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-italic.png new file mode 100644 index 00000000..b1a47a5e Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-italic.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-size.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-size.png new file mode 100644 index 00000000..57e8abae Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text-size.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text.png new file mode 100644 index 00000000..c92cafc3 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/text.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/zoom.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/zoom.png new file mode 100644 index 00000000..4f690d97 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/icons/zoom.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/button.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/button.png new file mode 100644 index 00000000..00a98817 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/button.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers-big.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers-big.png new file mode 100644 index 00000000..186b305b Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers-big.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers.png new file mode 100644 index 00000000..411023fb Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/checkers.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-close.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-close.png new file mode 100644 index 00000000..07b004dd Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-close.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-minimize.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-minimize.png new file mode 100644 index 00000000..03b62be0 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-minimize.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-restore.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-restore.png new file mode 100644 index 00000000..08737e48 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-restore.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-title.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-title.png new file mode 100644 index 00000000..59f0a71e Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/floatingPanel-title.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/paintweb-logo.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/paintweb-logo.png new file mode 100644 index 00000000..52a6b434 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/paintweb-logo.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/toolbar-colors.png b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/toolbar-colors.png new file mode 100644 index 00000000..18706d86 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/images/toolbar-colors.png differ diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/layout.xhtml b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/layout.xhtml new file mode 100644 index 00000000..3837df08 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/layout.xhtml @@ -0,0 +1,411 @@ +

+ +

PaintWeb

+ + +
+ + +
+ + +
    + + + + +
  • Undo
  • + +
  •  
  • + +
  • Selection
  • + +
  • Rectangle
  • + +
  •  
  • + + + + +
  • Crop selection
  • +
  • Fill selection
  • + +
  • +

    Fill +   +

    +

    Stroke +   +

    +
  • + +
  •  
  • + +
  • Rotate image
  • +
  • brighten image
  • +
  • darken image
  • + + + + +
  •  
  • + + + + + + + +
  • Clear image
  • +
  • Save image
  • + +
+ + +
+ + + + + + + + + + + + + + + + + +
+ +
+
+
+
Resize the image Canvas.
+
+ +
+

WxH

+ +

+ +

+ +

Status

+

Resize the image viewport.

+
+ + +
+ +

Color mixer

+ + +
+
    +
  1.   Active
  2. +
  3.   Old
  4. +
+ +
    +
  1. Close
  2. +
  3. Cancel
  4. +
  5. Save color
  6. +
  7. Pick color
  8. +
+ +
+
+ Your browser + does not support Canvas. +
+ + +
+
+ +
+ +
+
+
+ +
    +
  1. +
  2. +
  3. +
  4. +
+ +
+
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
+ +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
+ +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
+ +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + +
  8. +
+
+
+
+ +
+

About

+ +
+ + +

For user and developer documentation please check out the project site.

+
+
+ + +
diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/script.js b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/script.js new file mode 100644 index 00000000..cfa042c1 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/script.js @@ -0,0 +1,3249 @@ +/* + * Copyright (C) 2008, 2009, 2010 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2010-06-26 21:47:30 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview The default PaintWeb interface code. + */ + +/** + * @class The default PaintWeb interface. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.gui = function (app) { + var _self = this, + config = app.config, + doc = app.doc, + lang = app.lang, + MathRound = Math.round, + pwlib = window.pwlib, + appEvent = pwlib.appEvent, + win = app.win; + + this.app = app; + this.idPrefix = 'paintweb' + app.UID + '_', + this.classPrefix = 'paintweb_'; + + /** + * Holds references to DOM elements. + * @type Object + */ + this.elems = {}; + + /** + * Holds references to input elements associated to the PaintWeb configuration + * properties. + * @type Object + */ + this.inputs = {}; + + /** + * Holds references to DOM elements associated to configuration values. + * @type Object + */ + this.inputValues = {}; + + /** + * Holds references to DOM elements associated to color configuration + * properties. + * + * @type Object + * @see pwlib.guiColorInput + */ + this.colorInputs = {}; + + /** + * Holds references to DOM elements associated to each tool registered in the + * current PaintWeb application instance. + * + * @private + * @type Object + */ + this.tools = {}; + + /** + * Holds references to DOM elements associated to PaintWeb commands. + * + * @private + * @type Object + */ + this.commands = {}; + + /** + * Holds references to floating panels GUI components. + * + * @type Object + * @see pwlib.guiFloatingPanel + */ + this.floatingPanels = {zIndex_: 0}; + + /** + * Holds references to tab panel GUI components. + * + * @type Object + * @see pwlib.guiTabPanel + */ + this.tabPanels = {}; + + /** + * Holds an instance of the guiResizer object attached to the Canvas. + * + * @private + * @type pwlib.guiResizer + */ + this.canvasResizer = null; + + /** + * Holds an instance of the guiResizer object attached to the viewport + * element. + * + * @private + * @type pwlib.guiResizer + */ + this.viewportResizer = null; + + /** + * Holds tab configuration information for most drawing tools. + * + * @private + * @type Object + */ + this.toolTabConfig = { + bcurve: { + lineTab: true, + shapeType: true, + lineWidth: true, + lineWidthLabel: lang.inputs.borderWidth, + lineCap: true + }, + ellipse: { + lineTab: true, + shapeType: true, + lineWidth: true, + lineWidthLabel: lang.inputs.borderWidth + }, + rectangle: { + lineTab: true, + shapeType: true, + lineWidth: true, + lineWidthLabel: null, + lineJoin: true + }, + polygon: { + lineTab: true, + shapeType: true, + lineWidth: true, + lineWidthLabel: lang.inputs.borderWidth, + lineJoin: true, + lineCap: true, + miterLimit: true + }, + eraser: { + lineTab: true, + lineWidth: true, + lineWidthLabel: lang.inputs.eraserSize, + lineJoin: true, + lineCap: true, + miterLimit: true + }, + pencil: { + lineTab: true, + lineWidth: true, + lineWidthLabel: lang.inputs.pencilSize, + lineJoin: true, + lineCap: true, + miterLimit: true + }, + line: { + lineTab: true, + lineWidth: true, + lineWidthLabel: lang.inputs.line.lineWidth, + lineJoin: true, + lineCap: true, + miterLimit: true + }, + text: { + lineTab: false, + lineTabLabel: lang.tabs.main.textBorder, + shapeType: true, + lineWidth: true, + lineWidthLabel: lang.inputs.borderWidth + } + }; + + /** + * Initialize the PaintWeb interface. + * + * @param {Document|String} markup The interface markup loaded and parsed as + * DOM Document object. Optionally, the value can be a string holding the + * interface markup (this is used when PaintWeb is packaged). + * + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.init = function (markup) { + // Make sure the user nicely waits for PaintWeb to load, without seeing + // much. + var placeholder = config.guiPlaceholder, + placeholderStyle = placeholder.style; + + + placeholderStyle.display = 'none'; + placeholderStyle.height = '1px'; + placeholderStyle.overflow = 'hidden'; + placeholderStyle.position = 'absolute'; + placeholderStyle.visibility = 'hidden'; + + placeholder.className += ' ' + this.classPrefix + 'placeholder'; + if (!placeholder.tabIndex || placeholder.tabIndex == -1) { + placeholder.tabIndex = 1; + } + + if (!this.initImportDoc(markup)) { + app.initError(lang.guiMarkupImportFailed); + return false; + } + markup = null; + + if (!this.initParseMarkup()) { + app.initError(lang.guiMarkupParseFailed); + return false; + } + + if (!this.initCanvas() || + !this.initImageZoom() || + //!this.initSelectionTool() || + //!this.initTextTool() || + !this.initKeyboardShortcuts()) { + return false; + } + + // Setup the main tabbed panel. + var panel = this.tabPanels.main; + if (!panel) { + app.initError(lang.noMainTabPanel); + return false; + } + + // Hide the "Shadow" tab if the drawing of shadows is not supported. + if (!app.shadowSupported && 'shadow' in panel.tabs) { + panel.tabHide('shadow'); + } + + if (!('viewport' in this.elems)) { + app.initError(lang.missingViewport); + return false; + } + + // Setup the GUI dimensions . + this.elems.viewport.style.height = config.viewportHeight; + placeholderStyle.width = config.viewportWidth; + + // Setup the Canvas resizer. + var resizeHandle = this.elems.canvasResizer; + if (!resizeHandle) { + app.initError(lang.missingCanvasResizer); + return false; + } + resizeHandle.title = lang.guiCanvasResizer; + resizeHandle.replaceChild(doc.createTextNode(resizeHandle.title), + resizeHandle.firstChild); + resizeHandle.addEventListener('mouseover', this.item_mouseover, false); + resizeHandle.addEventListener('mouseout', this.item_mouseout, false); + + this.canvasResizer = new pwlib.guiResizer(this, resizeHandle, + this.elems.canvasContainer); + + this.canvasResizer.events.add('guiResizeStart', this.canvasResizeStart); + this.canvasResizer.events.add('guiResizeEnd', this.canvasResizeEnd); + + // Setup the viewport resizer. + var resizeHandle = this.elems.viewportResizer; + if (!resizeHandle) { + app.initError(lang.missingViewportResizer); + return false; + } + resizeHandle.title = lang.guiViewportResizer; + resizeHandle.replaceChild(doc.createTextNode(resizeHandle.title), + resizeHandle.firstChild); + resizeHandle.addEventListener('mouseover', this.item_mouseover, false); + resizeHandle.addEventListener('mouseout', this.item_mouseout, false); + + this.viewportResizer = new pwlib.guiResizer(this, resizeHandle, + this.elems.viewport); + + this.viewportResizer.dispatchMouseMove = true; + this.viewportResizer.events.add('guiResizeMouseMove', + this.viewportResizeMouseMove); + this.viewportResizer.events.add('guiResizeEnd', this.viewportResizeEnd); + + if ('statusMessage' in this.elems) { + this.elems.statusMessage._prevText = false; + } + + // Update the version string in Help. + if ('version' in this.elems) { + this.elems.version.appendChild(doc.createTextNode(app.toString())); + } + + // Update the image dimensions in the GUI. + var imageSize = this.elems.imageSize; + if (imageSize) { + imageSize.replaceChild(doc.createTextNode(app.image.width + 'x' + + app.image.height), imageSize.firstChild); + } + + // Add application-wide event listeners. + app.events.add('canvasSizeChange', this.canvasSizeChange); + app.events.add('commandRegister', this.commandRegister); + app.events.add('commandUnregister', this.commandUnregister); + app.events.add('configChange', this.configChangeHandler); + app.events.add('imageSizeChange', this.imageSizeChange); + app.events.add('imageZoom', this.imageZoom); + app.events.add('appInit', this.appInit); + app.events.add('shadowAllow', this.shadowAllow); + app.events.add('toolActivate', this.toolActivate); + app.events.add('toolRegister', this.toolRegister); + app.events.add('toolUnregister', this.toolUnregister); + + // Make sure the historyUndo and historyRedo command elements are + // synchronized with the application history state. + if ('historyUndo' in this.commands && 'historyRedo' in this.commands) { + app.events.add('historyUpdate', this.historyUpdate); + } + + app.commandRegister('about', this.commandAbout); + + return true; + }; + + /** + * Initialize the Canvas elements. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initCanvas = function () { + var canvasContainer = this.elems.canvasContainer, + layerCanvas = app.layer.canvas, + layerContext = app.layer.context, + layerStyle = layerCanvas.style, + bufferCanvas = app.buffer.canvas, + orginlCanvas = app.orginl.canvas; + + if (!canvasContainer) { + app.initError(lang.missingCanvasContainer); + return false; + } + + var containerStyle = canvasContainer.style; + + canvasContainer.className = this.classPrefix + 'canvasContainer'; + orginlCanvas.className = this.classPrefix + 'orginlCanvas'; + layerCanvas.className = this.classPrefix + 'layerCanvas'; + bufferCanvas.className = this.classPrefix + 'bufferCanvas'; + + containerStyle.width = layerStyle.width; + containerStyle.height = layerStyle.height; + if (!config.checkersBackground || pwlib.browser.olpcxo) { + containerStyle.backgroundImage = 'none'; + } + + canvasContainer.appendChild(orginlCanvas); + canvasContainer.appendChild(layerCanvas); + canvasContainer.appendChild(bufferCanvas); + + // Make sure the selection transparency input checkbox is disabled if the + // putImageData and getImageData methods are unsupported. + if ('selection_transparent' in this.inputs && (!layerContext.putImageData || + !layerContext.getImageData)) { + this.inputs.selection_transparent.disabled = true; + this.inputs.selection_transparent.checked = true; + } + + return true; + }; + + /** + * Import the DOM nodes from the interface DOM document. All the nodes are + * inserted into the {@link PaintWeb.config.guiPlaceholder} element. + * + *

Elements which have the ID attribute will have the attribute renamed to + * data-pwId. + * + *

Input elements which have the ID attribute will have their attribute + * updated to be unique for the current PaintWeb instance. + * + * @private + * + * @param {Document|String} markup The source DOM document to import the nodes + * from. Optionally, this parameter can be a string which holds the interface + * markup. + * + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initImportDoc = function (markup) { + // I could use some XPath here, but for the sake of compatibility I don't. + var destElem = config.guiPlaceholder, + elType = app.ELEMENT_NODE, + elem, root, nodes, n, tag, isInput; + + if (typeof markup === 'string') { + elem = doc.createElement('div'); + elem.innerHTML = markup; + root = elem.firstChild; + } else { + root = markup.documentElement; + } + markup = null; + + nodes = root.getElementsByTagName('*'); + n = nodes.length; + + // Change all the id attributes to be data-pwId attributes. + // Input elements have their ID updated to be unique for the current + // PaintWeb instance. + for (var i = 0; i < n; i++) { + elem = nodes[i]; + if (elem.nodeType !== elType || !elem.tagName) { + continue; + } + tag = elem.tagName.toLowerCase(); + isInput = tag === 'input' || tag === 'select' || tag === 'textarea'; + + if (elem.id) { + elem.setAttribute('data-pwId', elem.id); + + if (isInput) { + elem.id = this.idPrefix + elem.id; + } else { + elem.removeAttribute('id'); + } + } + + // label elements have their "for" attribute updated as well. + if (tag === 'label' && elem.htmlFor) { + elem.htmlFor = this.idPrefix + elem.htmlFor; + } + } + + // Import all the nodes. + n = root.childNodes.length; + + for (var i = 0; i < n; i++) { + destElem.appendChild(doc.importNode(root.childNodes[i], true)); + } + + return true; + }; + + /** + * Parse the interface markup. The layout file can have custom + * PaintWeb-specific attributes. + * + *

Elements with the data-pwId attribute are added to the + * {@link pwlib.gui#elems} object. + * + *

Elements having the data-pwCommand attribute are added to + * the {@link pwlib.gui#commands} object. + * + *

Elements having the data-pwTool attribute are added to the + * {@link pwlib.gui#tools} object. + * + *

Elements having the data-pwTabPanel attribute are added to + * the {@link pwlib.gui#tabPanels} object. These become interactive GUI + * components (see {@link pwlib.guiTabPanel}). + * + *

Elements having the data-pwFloatingPanel attribute are + * added to the {@link pwlib.gui#floatingPanels} object. These become + * interactive GUI components (see {@link pwlib.guiFloatingPanel}). + * + *

Elements having the data-pwConfig attribute are added to + * the {@link pwlib.gui#inputs} object. These become interactive GUI + * components which allow users to change configuration options. + * + *

Elements having the data-pwConfigValue attribute are added + * to the {@link pwlib.gui#inputValues} object. These can only be child nodes + * of elements which have the data-pwConfig attribute. Each such + * element is considered an icon. Anchor elements are appended to ensure + * keyboard accessibility. + * + *

Elements having the data-pwConfigToggle attribute are added + * to the {@link pwlib.gui#inputs} object. These become interactive GUI + * components which toggle the boolean value of the configuration property + * they are associated to. + * + *

Elements having the data-pwColorInput attribute are added + * to the {@link pwlib.gui#colorInputs} object. These become color picker + * inputs which are associated to the configuration property given as the + * attribute value. (see {@link pwlib.guiColorInput}) + * + * @returns {Boolean} True if the parsing was successful, or false if not. + */ + this.initParseMarkup = function () { + var nodes = config.guiPlaceholder.getElementsByTagName('*'), + elType = app.ELEMENT_NODE, + elem, tag, isInput, tool, tabPanel, floatingPanel, cmd, id, cfgAttr, + colorInput; + + // Store references to important elements and parse PaintWeb-specific + // attributes. + for (var i = 0; i < nodes.length; i++) { + elem = nodes[i]; + if (elem.nodeType !== elType) { + continue; + } + tag = elem.tagName.toLowerCase(); + isInput = tag === 'input' || tag === 'select' || tag === 'textarea'; + + // Store references to commands. + cmd = elem.getAttribute('data-pwCommand'); + if (cmd && !(cmd in this.commands)) { + elem.className += ' ' + this.classPrefix + 'command'; + this.commands[cmd] = elem; + } + + // Store references to tools. + tool = elem.getAttribute('data-pwTool'); + if (tool && !(tool in this.tools)) { + elem.className += ' ' + this.classPrefix + 'tool'; + this.tools[tool] = elem; + } + + // Create tab panels. + tabPanel = elem.getAttribute('data-pwTabPanel'); + if (tabPanel) { + this.tabPanels[tabPanel] = new pwlib.guiTabPanel(this, elem); + } + + // Create floating panels. + floatingPanel = elem.getAttribute('data-pwFloatingPanel'); + if (floatingPanel) { + this.floatingPanels[floatingPanel] = new pwlib.guiFloatingPanel(this, + elem); + } + + cfgAttr = elem.getAttribute('data-pwConfig'); + if (cfgAttr) { + if (isInput) { + this.initConfigInput(elem, cfgAttr); + } else { + this.initConfigIcons(elem, cfgAttr); + } + } + + cfgAttr = elem.getAttribute('data-pwConfigToggle'); + if (cfgAttr) { + this.initConfigToggle(elem, cfgAttr); + } + + cfgAttr = elem.getAttribute('data-pwConfigButton'); + if (cfgAttr) { + this.initConfigToggle(elem, cfgAttr, 'button'); + } + + // elem.hasAttribute() fails in webkit (tested with chrome and safari 4) + if (elem.getAttribute('data-pwColorInput')) { + colorInput = new pwlib.guiColorInput(this, elem); + this.colorInputs[colorInput.id] = colorInput; + } + + id = elem.getAttribute('data-pwId'); + if (id) { + elem.className += ' ' + this.classPrefix + id; + + // Store a reference to the element. + if (isInput && !cfgAttr) { + this.inputs[id] = elem; + } else if (!isInput) { + this.elems[id] = elem; + } + } + } + + return true; + }; + + /** + * Initialize an input element associated to a configuration property. + * + * @private + * + * @param {Element} elem The DOM element which is associated to the + * configuration property. + * + * @param {String} cfgAttr The configuration attribute. This tells the + * configuration group and property to which the DOM element is attached to. + */ + this.initConfigInput = function (input, cfgAttr) { + var cfgNoDots = cfgAttr.replace('.', '_'), + cfgArray = cfgAttr.split('.'), + cfgProp = cfgArray.pop(), + cfgGroup = cfgArray.join('.'), + cfgGroupRef = config, + langGroup = lang.inputs, + labelElem = input.parentNode; + + for (var i = 0, n = cfgArray.length; i < n; i++) { + cfgGroupRef = cfgGroupRef[cfgArray[i]]; + langGroup = langGroup[cfgArray[i]]; + } + + input._pwConfigProperty = cfgProp; + input._pwConfigGroup = cfgGroup; + input._pwConfigGroupRef = cfgGroupRef; + input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp]; + input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots; + + this.inputs[cfgNoDots] = input; + + if (labelElem.tagName.toLowerCase() !== 'label') { + labelElem = labelElem.getElementsByTagName('label')[0]; + } + + if (input.type === 'checkbox' || labelElem.htmlFor) { + labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), + labelElem.lastChild); + } else { + labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), + labelElem.firstChild); + } + + if (input.type === 'checkbox') { + input.checked = cfgGroupRef[cfgProp]; + } else { + input.value = cfgGroupRef[cfgProp]; + } + + input.addEventListener('input', this.configInputChange, false); + input.addEventListener('change', this.configInputChange, false); + }; + + /** + * Initialize an HTML element associated to a configuration property, and all + * of its own sub-elements associated to configuration values. Each element + * that has the data-pwConfigValue attribute is considered an icon. + * + * @private + * + * @param {Element} elem The DOM element which is associated to the + * configuration property. + * + * @param {String} cfgAttr The configuration attribute. This tells the + * configuration group and property to which the DOM element is attached to. + */ + this.initConfigIcons = function (input, cfgAttr) { + var cfgNoDots = cfgAttr.replace('.', '_'), + cfgArray = cfgAttr.split('.'), + cfgProp = cfgArray.pop(), + cfgGroup = cfgArray.join('.'), + cfgGroupRef = config, + langGroup = lang.inputs; + + for (var i = 0, n = cfgArray.length; i < n; i++) { + cfgGroupRef = cfgGroupRef[cfgArray[i]]; + langGroup = langGroup[cfgArray[i]]; + } + + input._pwConfigProperty = cfgProp; + input._pwConfigGroup = cfgGroup; + input._pwConfigGroupRef = cfgGroupRef; + input.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp]; + input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots; + + this.inputs[cfgNoDots] = input; + + var labelElem = input.getElementsByTagName('p')[0]; + labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), + labelElem.firstChild); + + var elem, anchor, val, + className = ' ' + this.classPrefix + 'configActive'; + nodes = input.getElementsByTagName('*'), + elType = app.ELEMENT_NODE; + + for (var i = 0; i < nodes.length; i++) { + elem = nodes[i]; + if (elem.nodeType !== elType) { + continue; + } + + val = elem.getAttribute('data-pwConfigValue'); + if (!val) { + continue; + } + + anchor = doc.createElement('a'); + anchor.href = '#'; + anchor.title = langGroup[cfgProp + '_' + val]; + anchor.appendChild(doc.createTextNode(anchor.title)); + + elem.className += ' ' + this.classPrefix + cfgProp + '_' + val + + ' ' + this.classPrefix + 'icon'; + elem._pwConfigParent = input; + + if (cfgGroupRef[cfgProp] == val) { + elem.className += className; + } + + anchor.addEventListener('click', this.configValueClick, false); + anchor.addEventListener('mouseover', this.item_mouseover, false); + anchor.addEventListener('mouseout', this.item_mouseout, false); + + elem.replaceChild(anchor, elem.firstChild); + + this.inputValues[cfgGroup + '_' + cfgProp + '_' + val] = elem; + } + }; + + /** + * Initialize an HTML element associated to a boolean configuration property. + * + * @private + * + * @param {Element} elem The DOM element which is associated to the + * configuration property. + * + * @param {String} cfgAttr The configuration attribute. This tells the + * configuration group and property to which the DOM element is attached to. + */ + this.initConfigToggle = function (input, cfgAttr, isButton) { + var cfgNoDots = cfgAttr.replace('.', '_'), + cfgArray = cfgAttr.split('.'), + cfgProp = cfgArray.pop(), + cfgGroup = cfgArray.join('.'), + cfgGroupRef = config, + langGroup = lang.inputs; + + for (var i = 0, n = cfgArray.length; i < n; i++) { + cfgGroupRef = cfgGroupRef[cfgArray[i]]; + langGroup = langGroup[cfgArray[i]]; + } + + input._pwConfigProperty = cfgProp; + input._pwConfigGroup = cfgGroup; + input._pwConfigGroupRef = cfgGroupRef; + input.className += ' ' + this.classPrefix + 'cfg_' + cfgNoDots + + ' ' + this.classPrefix + 'icon'; + + if (cfgGroupRef) + if (cfgGroupRef[cfgProp]) { + input.className += ' ' + this.classPrefix + 'configActive'; + } + + var anchor = doc.createElement('a'); + anchor.href = '#'; + anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp]; + anchor.appendChild(doc.createTextNode(langGroup[cfgProp])); + + if (isButton) + anchor.addEventListener('click', this.configButtonClick, false); + else anchor.addEventListener('click', this.configToggleClick, false); + anchor.addEventListener('mouseover', this.item_mouseover, false); + anchor.addEventListener('mouseout', this.item_mouseout, false); + + input.replaceChild(anchor, input.firstChild); + + this.inputs[cfgNoDots] = input; + }; + + /** + * Initialize the image zoom input. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initImageZoom = function () { + var input = this.inputs.imageZoom; + if (!input) { + return true; // allow layouts without the zoom input + } + + //--------------------------------------------------------------------------------------- + // FIXME : image size 조정값 zoom에 set + //--------------------------------------------------------------------------------------- + console.log('initImageZoom>>>>>', app.image.width, app.image.height, app.image.zoom); + input.value = app.image.zoom * 100; + input._old_value = 100; + //--------------------------------------------------------------------------------------- + //input.value = 100; + //input._old_value = 100; + + + + // Override the attributes, based on the settings. + input.setAttribute('step', config.imageZoomStep * 100); + input.setAttribute('max', config.imageZoomMax * 100); + input.setAttribute('min', config.imageZoomMin * 100); + + var changeFn = function () { + app.imageZoomTo(parseInt(this.value) / 100); + }; + + input.addEventListener('change', changeFn, false); + input.addEventListener('input', changeFn, false); + + // Update some language strings + var label = input.parentNode; + if (label.tagName.toLowerCase() === 'label') { + label.replaceChild(doc.createTextNode(lang.imageZoomLabel), + label.firstChild); + } + + var elem = this.elems.statusZoom; + if (!elem) { + return true; + } + + elem.title = lang.imageZoomTitle; + + return true; + }; + + /** + * Initialize GUI elements associated to selection tool options and commands. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initSelectionTool = function () { + var classDisabled = ' ' + this.classPrefix + 'disabled', + cut = this.commands.selectionCut, + copy = this.commands.selectionCopy, + paste = this.commands.clipboardPaste; + + if (paste) { + app.events.add('clipboardUpdate', this.clipboardUpdate); + paste.className += classDisabled; + + } + + if (cut && copy) { + app.events.add('selectionChange', this.selectionChange); + cut.className += classDisabled; + copy.className += classDisabled; + } + + var selTab_cmds = ['selectionCut', 'selectionCopy', 'clipboardPaste'], + anchor, elem, cmd; + + for (var i = 0, n = selTab_cmds.length; i < n; i++) { + cmd = selTab_cmds[i]; + elem = this.elems['selTab_' + cmd]; + if (!elem) { + continue; + } + + anchor = doc.createElement('a'); + anchor.title = lang.commands[cmd]; + anchor.href = '#'; + anchor.appendChild(doc.createTextNode(anchor.title)); + anchor.addEventListener('click', this.commandClick, false); + + elem.className += classDisabled + ' ' + this.classPrefix + 'command' + + ' ' + this.classPrefix + 'cmd_' + cmd; + elem.setAttribute('data-pwCommand', cmd); + elem.replaceChild(anchor, elem.firstChild); + } + + var selCrop = this.commands.selectionCrop, + selFill = this.commands.selectionFill, + selDelete = this.commands.selectionDelete; + + selCrop.className += classDisabled; + selFill.className += classDisabled; + selDelete.className += classDisabled; + + return true; + }; + + /** + * Initialize GUI elements associated to text tool options. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initTextTool = function () { + /*if ('textString' in this.inputs) { + this.inputs.textString.value = lang.inputs.text.textString_value; + }*/ + + if (!('text_fontFamily' in this.inputs) || !('text' in config) || + !('fontFamilies' in config.text)) { + return true; + } + + var option, input = this.inputs.text_fontFamily; + for (var i = 0, n = config.text.fontFamilies.length; i < n; i++) { + option = doc.createElement('option'); + option.value = config.text.fontFamilies[i]; + option.appendChild(doc.createTextNode(option.value)); + input.appendChild(option); + + if (option.value === config.text.fontFamily) { + input.selectedIndex = i; + input.value = option.value; + } + } + + option = doc.createElement('option'); + option.value = '+'; + option.appendChild(doc.createTextNode(lang.inputs.text.fontFamily_add)); + input.appendChild(option); + + var sizeArr = ["8", "10", "12", "14", "18", "24"]; + input = this.inputs.fontSize; + for (var i = 0; i < sizeArr.length; i++) { + option = doc.createElement('option'); + option.value = i+1; + option.appendChild(doc.createTextNode(sizeArr[i]+"pt")); + input.appendChild(option); + } + input.selectedIndex = 3; + + return true; + }; + + /** + * Initialize the keyboard shortcuts. Basically, this updates various strings + * to ensure the user interface is informational. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initKeyboardShortcuts = function () { + var kid = null, kobj = null; + + for (kid in config.keys) { + kobj = config.keys[kid]; + + if ('toolActivate' in kobj && kobj.toolActivate in lang.tools) { + lang.tools[kobj.toolActivate] += ' [ ' + kid + ' ]'; + } + + if ('command' in kobj && kobj.command in lang.commands) { + lang.commands[kobj.command] += ' [ ' + kid + ' ]'; + } + } + + return true; + }; + + /** + * The appInit event handler. This method is invoked once + * PaintWeb completes all the loading. + * + *

This method dispatches the {@link pwlib.appEvent.guiShow} application + * event. + * + * @private + * @param {pwlib.appEvent.appInit} ev The application event object. + */ + this.appInit = function (ev) { + // Initialization was not successful ... + if (ev.state !== PaintWeb.INIT_DONE) { + return; + } + + // Make sure the Hand tool is enabled/disabled as needed. + if ('hand' in _self.tools) { + app.events.add('canvasSizeChange', _self.toolHandStateChange); + app.events.add('viewportSizeChange', _self.toolHandStateChange); + _self.toolHandStateChange(ev); + } + + // Make PaintWeb visible. + var placeholder = config.guiPlaceholder, + placeholderStyle = placeholder.style; + + // We do not reset the display property. We leave this for the stylesheet. + placeholderStyle.height = ''; + placeholderStyle.overflow = ''; + placeholderStyle.position = ''; + placeholderStyle.visibility = ''; + + var cs = win.getComputedStyle(placeholder, null); + + // Do not allow the static positioning for the PaintWeb placeholder. + // Usually, the GUI requires absolute/relative positioning. + if (cs.position === 'static') { + placeholderStyle.position = 'relative'; + } + + try { + placeholder.focus(); + } catch (err) { } + + app.events.dispatch(new appEvent.guiShow()); + }; + + /** + * The guiResizeStart event handler for the Canvas resize + * operation. + * @private + */ + this.canvasResizeStart = function () { + this.resizeHandle.style.visibility = 'hidden'; + + // ugly... + this.timeout_ = setTimeout(function () { + _self.statusShow('guiCanvasResizerActive', true); + clearTimeout(_self.canvasResizer.timeout_); + delete _self.canvasResizer.timeout_; + }, 400); + }; + + /** + * The guiResizeEnd event handler for the Canvas resize + * operation. + * + * @private + * @param {pwlib.appEvent.guiResizeEnd} ev The application event object. + */ + this.canvasResizeEnd = function (ev) { + this.resizeHandle.style.visibility = ''; + + app.imageCrop(0, 0, MathRound(ev.width / app.image.canvasScale), + MathRound(ev.height / app.image.canvasScale)); + + if (this.timeout_) { + clearTimeout(this.timeout_); + delete this.timeout_; + } else { + _self.statusShow(-1); + } + }; + + /** + * The guiResizeMouseMove event handler for the viewport resize + * operation. + * + * @private + * @param {pwlib.appEvent.guiResizeMouseMove} ev The application event object. + */ + this.viewportResizeMouseMove = function (ev) { + config.guiPlaceholder.style.width = ev.width + 'px'; + }; + + /** + * The guiResizeEnd event handler for the viewport resize + * operation. + * + * @private + * @param {pwlib.appEvent.guiResizeEnd} ev The application event object. + */ + this.viewportResizeEnd = function (ev) { + _self.elems.viewport.style.width = ''; + _self.resizeTo(ev.width + 'px', ev.height + 'px'); + try { + config.guiPlaceholder.focus(); + } catch (err) { } + }; + + /** + * The mouseover event handler for all tools, commands and icons. + * This simply shows the title / text content of the element in the GUI status + * bar. + * + * @see pwlib.gui#statusShow The method used for displaying the message in the + * GUI status bar. + */ + this.item_mouseover = function () { + if (this.title || this.textConent) { + _self.statusShow(this.title || this.textContent, true); + } + }; + + /** + * The mouseout event handler for all tools, commands and icons. + * This method simply resets the GUI status bar to the previous message it was + * displaying before the user hovered the current element. + * + * @see pwlib.gui#statusShow The method used for displaying the message in the + * GUI status bar. + */ + this.item_mouseout = function () { + _self.statusShow(-1); + }; + + /** + * Show a message in the status bar. + * + * @param {String|Number} msg The message ID you want to display. The ID + * should be available in the {@link PaintWeb.lang.status} object. If the + * value is -1 then the previous non-temporary message will be displayed. If + * the ID is not available in the language file, then the string is shown + * as-is. + * + * @param {Boolean} [temporary=false] Tells if the message is temporary or + * not. + */ + this.statusShow = function (msg, temporary) { + var elem = this.elems.statusMessage; + if (msg === -1 && elem._prevText === false) { + return false; + } + + if (msg === -1) { + msg = elem._prevText; + } + + if (msg in lang.status) { + msg = lang.status[msg]; + } + + if (!temporary) { + elem._prevText = msg; + } + + if (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + + win.status = msg; + + if (msg) { + elem.appendChild(doc.createTextNode(msg)); + } + }; + + /** + * The "About" command. This method displays the "About" panel. + */ + this.commandAbout = function () { + _self.floatingPanels.about.toggle(); + }; + + /** + * The click event handler for the tool DOM elements. + * + * @private + * + * @param {Event} ev The DOM Event object. + * + * @see PaintWeb#toolActivate to activate a drawing tool. + */ + this.toolClick = function (ev) { + app.toolActivate(this.parentNode.getAttribute('data-pwTool'), ev); + ev.preventDefault(); + }; + + /** + * The toolActivate application event handler. This method + * provides visual feedback for the activation of a new drawing tool. + * + * @private + * + * @param {pwlib.appEvent.toolActivate} ev The application event object. + * + * @see PaintWeb#toolActivate the method which allows you to activate + * a drawing tool. + */ + this.toolActivate = function (ev) { + var tabAnchor, + tabActive = _self.tools[ev.id], + tabConfig = _self.toolTabConfig[ev.id] || {}, + tabPanel = _self.tabPanels.main, + lineTab = tabPanel.tabs.line, + shapeType = _self.inputs.shapeType, + lineWidth = _self.inputs.line_lineWidth, + lineCap = _self.inputs.line_lineCap, + lineJoin = _self.inputs.line_lineJoin, + miterLimit = _self.inputs.line_miterLimit, + lineWidthLabel = null; + + tabActive.className += ' ' + _self.classPrefix + 'toolActive'; + try { + tabActive.firstChild.focus(); + } catch (err) { } + + if ((ev.id + 'Active') in lang.status) { + _self.statusShow(ev.id + 'Active'); + } + + // show/hide the shapeType input config. + if (shapeType) { + if (tabConfig.shapeType) { + shapeType.style.display = ''; + } else { + shapeType.style.display = 'none'; + } + } + + if (ev.prevId) { + var prevTab = _self.tools[ev.prevId], + prevTabConfig = _self.toolTabConfig[ev.prevId] || {}; + + prevTab.className = prevTab.className. + replace(' ' + _self.classPrefix + 'toolActive', ''); + + // hide the line tab + if (prevTabConfig.lineTab && lineTab) { + tabPanel.tabHide('line'); + lineTab.container.className = lineTab.container.className. + replace(' ' + _self.classPrefix + 'main_line_' + ev.prevId, + ' ' + _self.classPrefix + 'main_line'); + } + + // hide the tab for the current tool. + if (ev.prevId in tabPanel.tabs) { + tabPanel.tabHide(ev.prevId); + } + } + + // Change the label of the lineWidth input element. + if (tabConfig.lineWidthLabel) { + lineWidthLabel = lineWidth.parentNode; + lineWidthLabel.replaceChild(doc.createTextNode(tabConfig.lineWidthLabel), + lineWidthLabel.firstChild); + + } + + if (lineJoin) { + if (tabConfig.lineJoin) { + lineJoin.style.display = ''; + } else { + lineJoin.style.display = 'none'; + } + } + + if (lineCap) { + if (tabConfig.lineCap) { + lineCap.style.display = ''; + } else { + lineCap.style.display = 'none'; + } + } + + if (miterLimit) { + if (tabConfig.miterLimit) { + miterLimit.parentNode.parentNode.style.display = ''; + } else { + miterLimit.parentNode.parentNode.style.display = 'none'; + } + } + + if (lineWidth) { + if (tabConfig.lineWidth) { + lineWidth.parentNode.parentNode.style.display = ''; + } else { + lineWidth.parentNode.parentNode.style.display = 'none'; + } + } + + // show the line tab, if configured + if (tabConfig.lineTab && 'line' in tabPanel.tabs) { + tabAnchor = lineTab.button.firstChild; + tabAnchor.title = tabConfig.lineTabLabel || lang.tabs.main[ev.id]; + tabAnchor.replaceChild(doc.createTextNode(tabAnchor.title), + tabAnchor.firstChild); + + if (ev.id !== 'line') { + lineTab.container.className = lineTab.container.className. + replace(' ' + _self.classPrefix + 'main_line', ' ' + _self.classPrefix + + 'main_line_' + ev.id); + } + + tabPanel.tabShow('line'); + } + + // show the tab for the current tool, if there's one. + if (ev.id in tabPanel.tabs) { + tabPanel.tabShow(ev.id); + } + }; + + /** + * The toolRegister application event handler. This method adds + * the new tool into the GUI. + * + * @private + * + * @param {pwlib.appEvent.toolRegister} ev The application event object. + * + * @see PaintWeb#toolRegister the method which allows you to register new + * tools. + */ + this.toolRegister = function (ev) { + var attr = null, elem = null, anchor = null; + + if (ev.id in _self.tools) { + elem = _self.tools[ev.id]; + attr = elem.getAttribute('data-pwTool'); + if (attr && attr !== ev.id) { + attr = null; + elem = null; + delete _self.tools[ev.id]; + } + } + + // Create a new element if there's none already associated to the tool ID. + if (!elem) { + elem = doc.createElement('li'); + } + + if (!attr) { + elem.setAttribute('data-pwTool', ev.id); + } + + elem.className += ' ' + _self.classPrefix + 'tool_' + ev.id; + + // Append an anchor element which holds the locale string. + anchor = doc.createElement('a'); + anchor.title = lang.tools[ev.id]; + anchor.href = '#'; + anchor.appendChild(doc.createTextNode(anchor.title)); + + if (elem.firstChild) { + elem.replaceChild(anchor, elem.firstChild); + } else { + elem.appendChild(anchor); + } + + anchor.addEventListener('click', _self.toolClick, false); + anchor.addEventListener('mouseover', _self.item_mouseover, false); + anchor.addEventListener('mouseout', _self.item_mouseout, false); + + if (!(ev.id in _self.tools)) { + _self.tools[ev.id] = elem; + _self.elems.tools.appendChild(elem); + } + + // Disable the text tool icon if the Canvas Text API is not supported. + if (ev.id === 'text' && !app.layer.context.fillText && + !app.layer.context.mozPathText && elem) { + elem.className += ' ' + _self.classPrefix + 'disabled'; + anchor.title = lang.tools.textUnsupported; + + anchor.removeEventListener('click', _self.toolClick, false); + anchor.addEventListener('click', function (ev) { + ev.preventDefault(); + }, false); + } + }; + + /** + * The toolUnregister application event handler. This method the + * tool element from the GUI. + * + * @param {pwlib.appEvent.toolUnregister} ev The application event object. + * + * @see PaintWeb#toolUnregister the method which allows you to unregister + * tools. + */ + this.toolUnregister = function (ev) { + if (ev.id in _self.tools) { + _self.elems.tools.removeChild(_self.tools[ev.id]); + delete _self.tools[ev.id]; + } else { + return; + } + }; + + /** + * The click event handler for the command DOM elements. + * + * @private + * + * @param {Event} ev The DOM Event object. + * + * @see PaintWeb#commandRegister to register a new command. + */ + this.commandClick = function (ev) { + var cmd = this.parentNode.getAttribute('data-pwCommand'); + if (cmd && cmd in app.commands) { + app.commands[cmd].call(this, ev); + } + ev.preventDefault(); + + try { + this.focus(); + } catch (err) { } + }; + + /** + * The commandRegister application event handler. GUI elements + * associated to commands are updated to ensure proper user interaction. + * + * @private + * + * @param {pwlib.appEvent.commandRegister} ev The application event object. + * + * @see PaintWeb#commandRegister the method which allows you to register new + * commands. + */ + this.commandRegister = function (ev) { + var elem = _self.commands[ev.id], + anchor = null; + if (!elem) { + return; + } + + elem.className += ' ' + _self.classPrefix + 'cmd_' + ev.id; + + if(elem.classList.contains('paintweb_buttonStyle')){ + button = doc.createElement('button'); + button.title = lang.commands[ev.id]; + button.type = "button"; + button.classList.add('btn'); + button.classList.add('blue'); + button.appendChild(doc.createTextNode(button.title)); + + if (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + elem.appendChild(button); + + button.addEventListener('click', _self.commandClick, false); + + } else { + anchor = doc.createElement('a'); + anchor.title = lang.commands[ev.id]; + anchor.href = '#'; + anchor.appendChild(doc.createTextNode(anchor.title)); + + // Remove the text content and append the locale string associated to + // current command inside an anchor element (for better keyboard + // accessibility). + if (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + elem.appendChild(anchor); + + anchor.addEventListener('click', _self.commandClick, false); + anchor.addEventListener('mouseover', _self.item_mouseover, false); + anchor.addEventListener('mouseout', _self.item_mouseout, false); + } + + + }; + + /** + * The commandUnregister application event handler. This method + * simply removes all the user interactivity from the GUI element associated + * to the command being unregistered. + * + * @private + * + * @param {pwlib.appEvent.commandUnregister} ev The application event object. + * + * @see PaintWeb#commandUnregister the method which allows you to unregister + * commands. + */ + this.commandUnregister = function (ev) { + var elem = _self.commands[ev.id], + anchor = null; + if (!elem) { + return; + } + + elem.className = elem.className.replace(' ' + _self.classPrefix + 'cmd_' + + ev.id, ''); + + anchor = elem.firstChild; + anchor.removeEventListener('click', this.commands[ev.id], false); + anchor.removeEventListener('mouseover', _self.item_mouseover, false); + anchor.removeEventListener('mouseout', _self.item_mouseout, false); + + elem.removeChild(anchor); + }; + + /** + * The historyUpdate application event handler. GUI elements + * associated to the historyUndo and to the + * historyRedo commands are updated such that they are either + * enabled or disabled, depending on the current history position. + * + * @private + * + * @param {pwlib.appEvent.historyUpdate} ev The application event object. + * + * @see PaintWeb#historyGoto the method which allows you to go to different + * history states. + */ + this.historyUpdate = function (ev) { + var undoElem = _self.commands.historyUndo, + undoState = false, + redoElem = _self.commands.historyRedo, + redoState = false, + className = ' ' + _self.classPrefix + 'disabled', + undoElemState = undoElem.className.indexOf(className) === -1, + redoElemState = redoElem.className.indexOf(className) === -1; + + if (ev.currentPos > 1) { + undoState = true; + } + if (ev.currentPos < ev.states) { + redoState = true; + } + + if (undoElemState !== undoState) { + if (undoState) { + undoElem.className = undoElem.className.replace(className, ''); + } else { + undoElem.className += className; + } + } + + if (redoElemState !== redoState) { + if (redoState) { + redoElem.className = redoElem.className.replace(className, ''); + } else { + redoElem.className += className; + } + } + }; + + /** + * The imageSizeChange application event handler. The GUI element + * which displays the image dimensions is updated to display the new image + * size. + * + *

Image size refers strictly to the dimensions of the image being edited + * by the user, that's width and height. + * + * @private + * @param {pwlib.appEvent.imageSizeChange} ev The application event object. + */ + this.imageSizeChange = function (ev) { + var imageSize = _self.elems.imageSize; + if (imageSize) { + imageSize.replaceChild(doc.createTextNode(ev.width + 'x' + ev.height), + imageSize.firstChild); + } + }; + + /** + * The canvasSizeChange application event handler. The Canvas + * container element dimensions are updated to the new values, and the image + * resize handle is positioned accordingly. + * + *

Canvas size refers strictly to the dimensions of the Canvas elements in + * the browser, changed with CSS style properties, width and height. Scaling + * of the Canvas elements is applied when the user zooms the image or when the + * browser changes the render DPI / zoom. + * + * @private + * @param {pwlib.appEvent.canvasSizeChange} ev The application event object. + */ + this.canvasSizeChange = function (ev) { + var canvasContainer = _self.elems.canvasContainer, + canvasResizer = _self.canvasResizer, + className = ' ' + _self.classPrefix + 'disabled', + resizeHandle = canvasResizer.resizeHandle; + + // Update the Canvas container to be the same size as the Canvas elements. + canvasContainer.style.width = ev.width + 'px'; + canvasContainer.style.height = ev.height + 'px'; + + resizeHandle.style.top = ev.height + 'px'; + resizeHandle.style.left = ev.width + 'px'; + }; + + /** + * The imageZoom application event handler. The GUI input element + * which displays the image zoom level is updated to display the new value. + * + * @private + * @param {pwlib.appEvent.imageZoom} ev The application event object. + */ + this.imageZoom = function (ev) { + var elem = _self.inputs.imageZoom, + val = MathRound(ev.zoom * 100); + if (elem && elem.value != val) { + elem.value = val; + } + }; + + /** + * The configChange application event handler. This method + * ensures the GUI input elements stay up-to-date when some PaintWeb + * configuration is modified. + * + * @private + * @param {pwlib.appEvent.configChange} ev The application event object. + */ + this.configChangeHandler = function (ev) { + var cfg = '', input; + if (ev.group) { + cfg = ev.group.replace('.', '_') + '_'; + } + cfg += ev.config; + input = _self.inputs[cfg]; + + // Handle changes for color inputs. + if (!input && (input = _self.colorInputs[cfg])) { + var color = ev.value.replace(/\s+/g, ''). + replace(/^rgba\(/, '').replace(/\)$/, ''); + + color = color.split(','); + input.updateColor({ + red: color[0] / 255, + green: color[1] / 255, + blue: color[2] / 255, + alpha: color[3] + }); + + return; + } + + if (!input) { + return; + } + + var tag = input.tagName.toLowerCase(), + isInput = tag === 'select' || tag === 'input' || tag === 'textarea'; + + if (isInput) { + if (input.type === 'checkbox' && input.checked !== ev.value) { + input.checked = ev.value; + } + if (input.type !== 'checkbox' && input.value !== ev.value) { + input.value = ev.value; + } + + return; + } + + var classActive = ' ' + _self.className + 'configActive'; + + if (input.hasAttribute('data-pwConfigToggle')) { + var inputActive = input.className.indexOf(classActive) !== -1; + + if (ev.value && !inputActive) { + input.className += classActive; + } else if (!ev.value && inputActive) { + input.className = input.className.replace(classActive, ''); + } + } + + var classActive = ' ' + _self.className + 'configActive', + prevValElem = _self.inputValues[cfg + '_' + ev.previousValue], + valElem = _self.inputValues[cfg + '_' + ev.value]; + + if (prevValElem && prevValElem.className.indexOf(classActive) !== -1) { + prevValElem.className = prevValElem.className.replace(classActive, ''); + } + + if (valElem && valElem.className.indexOf(classActive) === -1) { + valElem.className += classActive; + } + }; + + /** + * The click event handler for DOM elements associated to + * PaintWeb configuration values. These elements rely on parent elements which + * are associated to configuration properties. + * + *

This method dispatches the {@link pwlib.appEvent.configChange} event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.configValueClick = function (ev) { + var pNode = this.parentNode, + input = pNode._pwConfigParent, + val = pNode.getAttribute('data-pwConfigValue'); + + if (!input || !input._pwConfigProperty) { + return; + } + + ev.preventDefault(); + + var className = ' ' + _self.classPrefix + 'configActive', + groupRef = input._pwConfigGroupRef, + group = input._pwConfigGroup, + prop = input._pwConfigProperty, + prevVal = groupRef[prop], + prevValElem = _self.inputValues[group.replace('.', '_') + '_' + prop + + '_' + prevVal]; + + if (prevVal == val) { + return; + } + + if (prevValElem && prevValElem.className.indexOf(className) !== -1) { + prevValElem.className = prevValElem.className.replace(className, ''); + } + + groupRef[prop] = val; + + if (pNode.className.indexOf(className) === -1) { + pNode.className += className; + } + + app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, + groupRef)); + }; + + /** + * The change event handler for input elements associated to + * PaintWeb configuration properties. + * + *

This method dispatches the {@link pwlib.appEvent.configChange} event. + * + * @private + */ + this.configInputChange = function () { + if (!this._pwConfigProperty) { + return; + } + + var val = this.type === 'checkbox' ? this.checked : this.value, + groupRef = this._pwConfigGroupRef, + group = this._pwConfigGroup, + prop = this._pwConfigProperty, + prevVal = groupRef[prop]; + + if (this.getAttribute('type') === 'number') { + val = parseInt(val); + if (val != this.value) { + this.value = val; + } + } + + if (val == prevVal) { + return; + } + + groupRef[prop] = val; + + app.events.dispatch(new appEvent.configChange(val, prevVal, prop, group, + groupRef)); + }; + + /** + * The click event handler for DOM elements associated to boolean + * configuration properties. These elements only toggle the true/false value + * of the configuration property. + * + *

This method dispatches the {@link pwlib.appEvent.configChange} event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + this.configToggleClick = function (ev) { + var className = ' ' + _self.classPrefix + 'configActive', + pNode = this.parentNode, + groupRef = pNode._pwConfigGroupRef, + group = pNode._pwConfigGroup, + prop = pNode._pwConfigProperty, + elemActive = pNode.className.indexOf(className) !== -1; + + ev.preventDefault(); + + groupRef[prop] = !groupRef[prop]; + + if (groupRef[prop] && !elemActive) { + pNode.className += className; + } else if (!groupRef[prop] && elemActive) { + pNode.className = pNode.className.replace(className, ''); + } + + app.events.dispatch(new appEvent.configChange(groupRef[prop], + !groupRef[prop], prop, group, groupRef)); + }; + + /** + * It's same with configToggleClick(). + * just remove code for Toggle. + */ + this.configButtonClick = function (ev) { + var className = ' ' + _self.classPrefix + 'configActive', + pNode = this.parentNode, + groupRef = pNode._pwConfigGroupRef, + group = pNode._pwConfigGroup, + prop = pNode._pwConfigProperty, + elemActive = pNode.className.indexOf(className) !== -1; + + ev.preventDefault(); + + groupRef[prop] = !groupRef[prop]; + + app.events.dispatch(new appEvent.configChange(groupRef[prop], + !groupRef[prop], prop, group, groupRef)); + }; + /** + * The shadowAllow application event handler. This method + * shows/hide the shadow tab when shadows are allowed/disallowed. + * + * @private + * @param {pwlib.appEvent.shadowAllow} ev The application event object. + */ + this.shadowAllow = function (ev) { + if ('shadow' in _self.tabPanels.main.tabs) { + if (ev.allowed) { + _self.tabPanels.main.tabShow('shadow'); + } else { + _self.tabPanels.main.tabHide('shadow'); + } + } + }; + + /** + * The clipboardUpdate application event handler. The GUI element + * associated to the clipboardPaste command is updated to be + * disabled/enabled depending on the event. + * + * @private + * @param {pwlib.appEvent.clipboardUpdate} ev The application event object. + */ + this.clipboardUpdate = function (ev) { + var classDisabled = ' ' + _self.classPrefix + 'disabled', + elem, elemEnabled, + elems = [_self.commands.clipboardPaste, + _self.elems.selTab_clipboardPaste]; + + for (var i = 0, n = elems.length; i < n; i++) { + elem = elems[i]; + if (!elem) { + continue; + } + + elemEnabled = elem.className.indexOf(classDisabled) === -1; + + if (!ev.data && elemEnabled) { + elem.className += classDisabled; + } else if (ev.data && !elemEnabled) { + elem.className = elem.className.replace(classDisabled, ''); + } + } + }; + + /** + * The selectionChange application event handler. The GUI + * elements associated to the selectionCut and + * selectionCopy commands are updated to be disabled/enabled + * depending on the event. + * + * @private + * @param {pwlib.appEvent.selectionChange} ev The application event object. + */ + this.selectionChange = function (ev) { + var classDisabled = ' ' + _self.classPrefix + 'disabled', + elem, elemEnabled, + elems = [_self.commands.selectionCut, _self.commands.selectionCopy, + _self.elems.selTab_selectionCut, _self.elems.selTab_selectionCopy, + _self.commands.selectionDelete, _self.commands.selectionFill, + _self.commands.selectionCrop]; + + for (var i = 0, n = elems.length; i < n; i++) { + elem = elems[i]; + if (!elem) { + continue; + } + + elemEnabled = elem.className.indexOf(classDisabled) === -1; + + if (ev.state === ev.STATE_NONE && elemEnabled) { + elem.className += classDisabled; + } else if (ev.state === ev.STATE_SELECTED && !elemEnabled) { + elem.className = elem.className.replace(classDisabled, ''); + } + } + }; + + /** + * Show the graphical user interface. + * + *

This method dispatches the {@link pwlib.appEvent.guiShow} application + * event. + */ + this.show = function () { + var placeholder = config.guiPlaceholder, + className = this.classPrefix + 'placeholder', + re = new RegExp('\\b' + className); + + if (!re.test(placeholder.className)) { + placeholder.className += ' ' + className; + } + + try { + placeholder.focus(); + } catch (err) { } + + app.events.dispatch(new appEvent.guiShow()); + }; + + /** + * Hide the graphical user interface. + * + *

This method dispatches the {@link pwlib.appEvent.guiHide} application + * event. + */ + this.hide = function () { + var placeholder = config.guiPlaceholder, + re = new RegExp('\\b' + this.classPrefix + 'placeholder', 'g'); + + placeholder.className = placeholder.className.replace(re, ''); + + app.events.dispatch(new appEvent.guiHide()); + }; + + /** + * The application destroy event handler. This method is invoked by the main + * PaintWeb application when the instance is destroyed, for the purpose of + * cleaning-up the GUI-related things from the document add by the current + * instance. + * + * @private + */ + this.destroy = function () { + var placeholder = config.guiPlaceholder; + + while(placeholder.hasChildNodes()) { + placeholder.removeChild(placeholder.firstChild); + } + }; + + /** + * Resize the PaintWeb graphical user interface. + * + *

This method dispatches the {@link pwlib.appEvent.configChange} event for + * the "viewportWidth" and "viewportHeight" configuration properties. Both + * properties are updated to hold the new values you give. + * + *

Once the GUI is resized, the {@link pwlib.appEvent.viewportSizeChange} + * event is also dispatched. + * + * @param {String} width The new width you want. Make sure the value is a CSS + * length, like "50%", "450px" or "30em". + * + * @param {String} height The new height you want. + */ + this.resizeTo = function (width, height) { + if (!width || !height) { + return; + } + + var width_old = config.viewportWidth, + height_old = config.viewportHeight; + + config.viewportWidth = width; + config.viewportHeight = height; + + app.events.dispatch(new appEvent.configChange(width, width_old, + 'viewportWidth', '', config)); + + app.events.dispatch(new appEvent.configChange(height, height_old, + 'viewportHeight', '', config)); + + config.guiPlaceholder.style.width = config.viewportWidth; + this.elems.viewport.style.height = config.viewportHeight; + + app.events.dispatch(new appEvent.viewportSizeChange(width, height)); + }; + + /** + * The state change event handler for the Hand tool. This function + * enables/disables the Hand tool by checking if the current image fits into + * the viewport or not. + * + *

This function is invoked when one of the following application events is + * dispatched: viewportSizeChange, canvasSizeChange + * or appInitdata-pwFloatingPanel element attribute. + * @type String + */ + this.id = null; + + /** + * Reference to the floating panel element. + * @type Element + */ + this.container = container; + + /** + * The viewport element. This element is the first parent element which has + * the style.overflow set to "auto" or "scroll". + * @type Element + */ + this.viewport = null; + + /** + * Custom application events interface. + * @type pwlib.appEvents + */ + this.events = null; + + /** + * The panel content element. + * @type Element + */ + this.content = null; + + // The initial viewport scroll position. + var vScrollLeft = 0, vScrollTop = 0, + btn_close = null, btn_minimize = null; + + /** + * Initialize the floating panel. + * @private + */ + function init () { + _self.events = new pwlib.appEvents(_self); + + _self.id = _self.container.getAttribute('data-pwFloatingPanel'); + + var ttl = _self.container.getElementsByTagName('h1')[0], + content = _self.container.getElementsByTagName('div')[0], + cs = win.getComputedStyle(_self.container, null), + zIndex = parseInt(cs.zIndex); + + cStyle.zIndex = cs.zIndex; + + if (zIndex > panels.zIndex_) { + panels.zIndex_ = zIndex; + } + + _self.container.className += ' ' + gui.classPrefix + 'floatingPanel ' + + gui.classPrefix + 'floatingPanel_' + _self.id; + + // the content + content.className += ' ' + gui.classPrefix + 'floatingPanel_content'; + _self.content = content; + + // setup the title element + ttl.className += ' ' + gui.classPrefix + 'floatingPanel_title'; + ttl.replaceChild(doc.createTextNode(lang.floatingPanels[_self.id]), + ttl.firstChild); + + ttl.addEventListener('mousedown', ev_mousedown, false); + + // allow auto-hide for the panel + if (_self.container.getAttribute('data-pwPanelHide') === 'true') { + _self.hide(); + } else { + _self.state = _self.STATE_VISIBLE; + } + + // Find the viewport parent element. + var pNode = _self.container.parentNode, + found = null; + + while (!found && pNode) { + if (pNode.nodeName.toLowerCase() === 'html') { + found = pNode; + break; + } + + cs = win.getComputedStyle(pNode, null); + if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) { + found = pNode; + } else { + pNode = pNode.parentNode; + } + } + + _self.viewport = found; + + // add the panel minimize button. + btn_minimize = doc.createElement('a'); + btn_minimize.href = '#'; + btn_minimize.title = lang.floatingPanelMinimize; + btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize'; + btn_minimize.addEventListener('click', ev_minimize, false); + btn_minimize.appendChild(doc.createTextNode(btn_minimize.title)); + + _self.container.insertBefore(btn_minimize, content); + + // add the panel close button. + btn_close = doc.createElement('a'); + btn_close.href = '#'; + btn_close.title = lang.floatingPanelClose; + btn_close.className = gui.classPrefix + 'floatingPanel_close'; + btn_close.addEventListener('click', ev_close, false); + btn_close.appendChild(doc.createTextNode(btn_close.title)); + + _self.container.insertBefore(btn_close, content); + + // setup the panel resize handle. + if (_self.container.getAttribute('data-pwPanelResizable') === 'true') { + var resizeHandle = doc.createElement('div'); + resizeHandle.className = gui.classPrefix + 'floatingPanel_resizer'; + _self.container.appendChild(resizeHandle); + _self.resizer = new pwlib.guiResizer(gui, resizeHandle, _self.container); + } + }; + + /** + * The click event handler for the panel Minimize button element. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_minimize (ev) { + ev.preventDefault(); + try { + this.focus(); + } catch (err) { } + + var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized'; + + if (_self.state === _self.STATE_MINIMIZED) { + _self.state = _self.STATE_VISIBLE; + + this.title = lang.floatingPanelMinimize; + this.className = gui.classPrefix + 'floatingPanel_minimize'; + this.replaceChild(doc.createTextNode(this.title), this.firstChild); + + if (_self.container.className.indexOf(classMinimized) !== -1) { + _self.container.className + = _self.container.className.replace(classMinimized, ''); + } + + } else if (_self.state === _self.STATE_VISIBLE) { + _self.state = _self.STATE_MINIMIZED; + + this.title = lang.floatingPanelRestore; + this.className = gui.classPrefix + 'floatingPanel_restore'; + this.replaceChild(doc.createTextNode(this.title), this.firstChild); + + if (_self.container.className.indexOf(classMinimized) === -1) { + _self.container.className += classMinimized; + } + } + + _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state)); + + _self.bringOnTop(); + }; + + /** + * The click event handler for the panel Close button element. + * This hides the floating panel. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_close (ev) { + ev.preventDefault(); + _self.hide(); + try { + guiPlaceholder.focus(); + } catch (err) { } + }; + + /** + * The mousedown event handler. This is invoked when you start + * dragging the floating panel. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mousedown (ev) { + _self.state = _self.STATE_DRAGGING; + + mx = ev.clientX; + my = ev.clientY; + + var cs = win.getComputedStyle(_self.container, null); + + ptop = parseInt(cs.top); + pleft = parseInt(cs.left); + + if (_self.viewport) { + vScrollLeft = _self.viewport.scrollLeft; + vScrollTop = _self.viewport.scrollTop; + } + + _self.bringOnTop(); + + doc.addEventListener('mousemove', ev_mousemove, false); + doc.addEventListener('mouseup', ev_mouseup, false); + + _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state)); + + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + + /** + * The mousemove event handler. This performs the actual move of + * the floating panel. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mousemove (ev) { + var x = pleft + ev.clientX - mx, + y = ptop + ev.clientY - my; + + if (_self.viewport) { + if (_self.viewport.scrollLeft !== vScrollLeft) { + x += _self.viewport.scrollLeft - vScrollLeft; + } + if (_self.viewport.scrollTop !== vScrollTop) { + y += _self.viewport.scrollTop - vScrollTop; + } + } + + cStyle.left = x + 'px'; + cStyle.top = y + 'px'; + }; + + /** + * The mouseup event handler. This ends the panel drag operation. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mouseup (ev) { + if (_self.container.className.indexOf(' ' + gui.classPrefix + + 'floatingPanel_minimized') !== -1) { + _self.state = _self.STATE_MINIMIZED; + } else { + _self.state = _self.STATE_VISIBLE; + } + + doc.removeEventListener('mousemove', ev_mousemove, false); + doc.removeEventListener('mouseup', ev_mouseup, false); + + try { + guiPlaceholder.focus(); + } catch (err) { } + + _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state)); + }; + + /** + * Bring the panel to the top. This method makes sure the current floating + * panel is visible. + */ + this.bringOnTop = function () { + panels.zIndex_ += zIndex_step; + cStyle.zIndex = panels.zIndex_; + }; + + /** + * Hide the panel. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + */ + this.hide = function () { + cStyle.display = 'none'; + _self.state = _self.STATE_HIDDEN; + _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state)); + }; + + /** + * Show the panel. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + */ + this.show = function () { + if (_self.state === _self.STATE_VISIBLE) { + return; + } + + cStyle.display = 'block'; + _self.state = _self.STATE_VISIBLE; + + var classMinimized = ' ' + gui.classPrefix + 'floatingPanel_minimized'; + + if (_self.container.className.indexOf(classMinimized) !== -1) { + _self.container.className + = _self.container.className.replace(classMinimized, ''); + + btn_minimize.className = gui.classPrefix + 'floatingPanel_minimize'; + btn_minimize.title = lang.floatingPanelMinimize; + btn_minimize.replaceChild(doc.createTextNode(btn_minimize.title), + btn_minimize.firstChild); + } + + _self.events.dispatch(new appEvent.guiFloatingPanelStateChange(_self.state)); + + _self.bringOnTop(); + }; + + /** + * Toggle the panel visibility. + * + *

This method dispatches the {@link + * pwlib.appEvent.guiFloatingPanelStateChange} application event. + */ + this.toggle = function () { + if (_self.state === _self.STATE_VISIBLE || _self.state === + _self.STATE_MINIMIZED) { + _self.hide(); + } else { + _self.show(); + } + }; + + init(); +}; + +/** + * @class The state change event for the floating panel. This event is fired + * when the floating panel changes its state. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} state The floating panel state. + */ +pwlib.appEvent.guiFloatingPanelStateChange = function (state) { + /** + * Panel state: hidden. + * @constant + */ + this.STATE_HIDDEN = 0; + + /** + * Panel state: visible. + * @constant + */ + this.STATE_VISIBLE = 1; + + /** + * Panel state: minimized. + * @constant + */ + this.STATE_MINIMIZED = 3; + + /** + * Panel state: the user is dragging the floating panel. + * @constant + */ + this.STATE_DRAGGING = 4; + + /** + * The current floating panel state. + * @type Number + */ + this.state = state; + + pwlib.appEvent.call(this, 'guiFloatingPanelStateChange'); +}; + +/** + * @class Resize handler. + * + * @private + * + * @param {pwlib.gui} gui Reference to the PaintWeb GUI object. + * + * @param {Element} resizeHandle Reference to the resize handle DOM element. + * This is the element users will be able to drag to achieve the resize effect + * on the container element. + * + * @param {Element} container Reference to the container DOM element. This is + * the element users will be able to resize using the resizeHandle + * element. + */ +pwlib.guiResizer = function (gui, resizeHandle, container) { + var _self = this, + cStyle = container.style, + doc = gui.app.doc, + guiResizeEnd = pwlib.appEvent.guiResizeEnd, + guiResizeMouseMove = pwlib.appEvent.guiResizeMouseMove, + guiResizeStart = pwlib.appEvent.guiResizeStart, + win = gui.app.win; + + /** + * Custom application events interface. + * @type pwlib.appEvents + */ + this.events = null; + + /** + * The resize handle DOM element. + * @type Element + */ + this.resizeHandle = resizeHandle; + + /** + * The container DOM element. This is the element that's resized by the user + * when he/she drags the resize handle. + * @type Element + */ + this.container = container; + + /** + * The viewport element. This element is the first parent element which has + * the style.overflow set to "auto" or "scroll". + * @type Element + */ + this.viewport = null; + + /** + * Tells if the GUI resizer should dispatch the {@link + * pwlib.appEvent.guiResizeMouseMove} application event when the user moves + * the mouse during the resize operation. + * + * @type Boolean + * @default false + */ + this.dispatchMouseMove = false; + + /** + * Tells if the user resizing the container now. + * + * @type Boolean + * @default false + */ + this.resizing = false; + + // The initial position of the mouse. + var mx = 0, my = 0; + + // The initial container dimensions. + var cWidth = 0, cHeight = 0; + + // The initial viewport scroll position. + var vScrollLeft = 0, vScrollTop = 0; + + /** + * Initialize the resize functionality. + * @private + */ + function init () { + _self.events = new pwlib.appEvents(_self); + resizeHandle.addEventListener('mousedown', ev_mousedown, false); + + // Find the viewport parent element. + var cs, pNode = _self.container.parentNode, + found = null; + while (!found && pNode) { + if (pNode.nodeName.toLowerCase() === 'html') { + found = pNode; + break; + } + + cs = win.getComputedStyle(pNode, null); + if (cs && (cs.overflow === 'scroll' || cs.overflow === 'auto')) { + found = pNode; + } else { + pNode = pNode.parentNode; + } + } + + _self.viewport = found; + }; + + /** + * The mousedown event handler. This starts the resize operation. + * + *

This function dispatches the {@link pwlib.appEvent.guiResizeStart} + * event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mousedown (ev) { + mx = ev.clientX; + my = ev.clientY; + + var cs = win.getComputedStyle(_self.container, null); + cWidth = parseInt(cs.width); + cHeight = parseInt(cs.height); + + var cancel = _self.events.dispatch(new guiResizeStart(mx, my, cWidth, + cHeight)); + + if (cancel) { + return; + } + + if (_self.viewport) { + vScrollLeft = _self.viewport.scrollLeft; + vScrollTop = _self.viewport.scrollTop; + } + + _self.resizing = true; + doc.addEventListener('mousemove', ev_mousemove, false); + doc.addEventListener('mouseup', ev_mouseup, false); + + if (ev.preventDefault) { + ev.preventDefault(); + } + + if (ev.stopPropagation) { + ev.stopPropagation(); + } + }; + + /** + * The mousemove event handler. This performs the actual resizing + * of the container element. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mousemove (ev) { + var w = cWidth + ev.clientX - mx, + h = cHeight + ev.clientY - my; + + if (_self.viewport) { + if (_self.viewport.scrollLeft !== vScrollLeft) { + w += _self.viewport.scrollLeft - vScrollLeft; + } + if (_self.viewport.scrollTop !== vScrollTop) { + h += _self.viewport.scrollTop - vScrollTop; + } + } + + cStyle.width = w + 'px'; + cStyle.height = h + 'px'; + + if (_self.dispatchMouseMove) { + _self.events.dispatch(new guiResizeMouseMove(ev.clientX, ev.clientY, w, + h)); + } + }; + + /** + * The mouseup event handler. This ends the resize operation. + * + *

This function dispatches the {@link pwlib.appEvent.guiResizeEnd} event. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_mouseup (ev) { + var cancel = _self.events.dispatch(new guiResizeEnd(ev.clientX, ev.clientY, + parseInt(cStyle.width), parseInt(cStyle.height))); + + if (cancel) { + return; + } + + _self.resizing = false; + doc.removeEventListener('mousemove', ev_mousemove, false); + doc.removeEventListener('mouseup', ev_mouseup, false); + }; + + init(); +}; + +/** + * @class The GUI element resize start event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} x The mouse location on the x-axis. + * @param {Number} y The mouse location on the y-axis. + * @param {Number} width The element width. + * @param {Number} height The element height. + */ +pwlib.appEvent.guiResizeStart = function (x, y, width, height) { + /** + * The mouse location on the x-axis. + * @type Number + */ + this.x = x; + + /** + * The mouse location on the y-axis. + * @type Number + */ + this.y = y; + + /** + * The element width. + * @type Number + */ + this.width = width; + + /** + * The element height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'guiResizeStart', true); +}; + +/** + * @class The GUI element resize end event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} x The mouse location on the x-axis. + * @param {Number} y The mouse location on the y-axis. + * @param {Number} width The element width. + * @param {Number} height The element height. + */ +pwlib.appEvent.guiResizeEnd = function (x, y, width, height) { + /** + * The mouse location on the x-axis. + * @type Number + */ + this.x = x; + + /** + * The mouse location on the y-axis. + * @type Number + */ + this.y = y; + + /** + * The element width. + * @type Number + */ + this.width = width; + + /** + * The element height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'guiResizeEnd', true); +}; + +/** + * @class The GUI element resize mouse move event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} x The mouse location on the x-axis. + * @param {Number} y The mouse location on the y-axis. + * @param {Number} width The element width. + * @param {Number} height The element height. + */ +pwlib.appEvent.guiResizeMouseMove = function (x, y, width, height) { + /** + * The mouse location on the x-axis. + * @type Number + */ + this.x = x; + + /** + * The mouse location on the y-axis. + * @type Number + */ + this.y = y; + + /** + * The element width. + * @type Number + */ + this.width = width; + + /** + * The element height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'guiResizeMouseMove'); +}; + +/** + * @class The tabbed panel GUI component. + * + * @private + * + * @param {pwlib.gui} gui Reference to the PaintWeb GUI object. + * + * @param {Element} panel Reference to the panel DOM element. + */ +pwlib.guiTabPanel = function (gui, panel) { + var _self = this, + appEvent = pwlib.appEvent, + doc = gui.app.doc, + lang = gui.app.lang; + + /** + * Custom application events interface. + * @type pwlib.appEvents + */ + this.events = null; + + /** + * Panel ID. The ID is the same as the data-pwTabPanel attribute value of the + * panel DOM element . + * + * @type String. + */ + this.id = null; + + /** + * Holds references to the DOM element of each tab and tab button. + * @type Object + */ + this.tabs = {}; + + /** + * Reference to the tab buttons DOM element. + * @type Element + */ + this.tabButtons = null; + + /** + * The panel container DOM element. + * @type Element + */ + this.container = panel; + + /** + * Holds the ID of the currently active tab. + * @type String + */ + this.tabId = null; + + /** + * Holds the ID of the previously active tab. + * + * @private + * @type String + */ + var prevTabId_ = null; + + /** + * Initialize the toolbar functionality. + * @private + */ + function init () { + _self.events = new pwlib.appEvents(_self); + _self.id = _self.container.getAttribute('data-pwTabPanel'); + + // Add two class names, the generic .paintweb_tabPanel and another class + // name specific to the current tab panel: .paintweb_tabPanel_id. + _self.container.className += ' ' + gui.classPrefix + 'tabPanel' + + ' ' + gui.classPrefix + 'tabPanel_' + _self.id; + + var tabButtons = doc.createElement('ul'), + tabButton = null, + tabDefault = _self.container.getAttribute('data-pwTabDefault') || null, + childNodes = _self.container.childNodes, + type = gui.app.ELEMENT_NODE, + elem = null, + tabId = null, + anchor = null; + + tabButtons.className = gui.classPrefix + 'tabsList'; + + // Find all the tabs in the current panel container element. + for (var i = 0; elem = childNodes[i]; i++) { + if (elem.nodeType !== type) { + continue; + } + + // A tab is any element with a given data-pwTab attribute. + tabId = elem.getAttribute('data-pwTab'); + if (!tabId) { + continue; + } + + // two class names, the generic .paintweb_tab and the tab-specific class + // name .paintweb_tabPanelId_tabId. + elem.className += ' ' + gui.classPrefix + 'tab ' + gui.classPrefix + + _self.id + '_' + tabId; + + tabButton = doc.createElement('li'); + tabButton._pwTab = tabId; + + anchor = doc.createElement('a'); + anchor.href = '#'; + anchor.addEventListener('click', ev_tabClick, false); + + if (_self.id in lang.tabs) { + anchor.title = lang.tabs[_self.id][tabId + 'Title'] || + lang.tabs[_self.id][tabId]; + anchor.appendChild(doc.createTextNode(lang.tabs[_self.id][tabId])); + } + + if ((tabDefault && tabId === tabDefault) || + (!tabDefault && !_self.tabId)) { + _self.tabId = tabId; + tabButton.className = gui.classPrefix + 'tabActive'; + } else { + prevTabId_ = tabId; + elem.style.display = 'none'; + } + + // automatically hide the tab + if (elem.getAttribute('data-pwTabHide') === 'true') { + tabButton.style.display = 'none'; + } + + _self.tabs[tabId] = {container: elem, button: tabButton}; + + tabButton.appendChild(anchor); + tabButtons.appendChild(tabButton); + } + + _self.tabButtons = tabButtons; + _self.container.appendChild(tabButtons); + }; + + /** + * The click event handler for tab buttons. This function simply + * activates the tab the user clicked. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_tabClick (ev) { + ev.preventDefault(); + _self.tabActivate(this.parentNode._pwTab); + }; + + /** + * Activate a tab by ID. + * + *

This method dispatches the {@link pwlib.appEvent.guiTabActivate} event. + * + * @param {String} tabId The ID of the tab you want to activate. + * @returns {Boolean} True if the tab has been activated successfully, or + * false if not. + */ + this.tabActivate = function (tabId) { + if (!tabId || !(tabId in this.tabs)) { + return false; + } else if (tabId === this.tabId) { + return true; + } + + var ev = new appEvent.guiTabActivate(tabId, this.tabId), + cancel = this.events.dispatch(ev), + elem = null, + tabButton = null; + + if (cancel) { + return false; + } + + // Deactivate the currently active tab. + if (this.tabId in this.tabs) { + elem = this.tabs[this.tabId].container; + elem.style.display = 'none'; + tabButton = this.tabs[this.tabId].button; + tabButton.className = ''; + prevTabId_ = this.tabId; + } + + // Activate the new tab. + elem = this.tabs[tabId].container; + elem.style.display = ''; + tabButton = this.tabs[tabId].button; + tabButton.className = gui.classPrefix + 'tabActive'; + tabButton.style.display = ''; // make sure the tab is not hidden + this.tabId = tabId; + + try { + tabButton.firstChild.focus(); + } catch (err) { } + + return true; + }; + + /** + * Hide a tab by ID. + * + * @param {String} tabId The ID of the tab you want to hide. + * @returns {Boolean} True if the tab has been hidden successfully, or false + * if not. + */ + this.tabHide = function (tabId) { + if (!(tabId in this.tabs)) { + return false; + } + + if (this.tabId === tabId) { + this.tabActivate(prevTabId_); + } + + this.tabs[tabId].button.style.display = 'none'; + + return true; + }; + + /** + * Show a tab by ID. + * + * @param {String} tabId The ID of the tab you want to show. + * @returns {Boolean} True if the tab has been displayed successfully, or + * false if not. + */ + this.tabShow = function (tabId) { + if (!(tabId in this.tabs)) { + return false; + } + + this.tabs[tabId].button.style.display = ''; + + return true; + }; + + init(); +}; + +/** + * @class The GUI tab activation event. This event is cancelable. + * + * @augments pwlib.appEvent + * + * @param {String} tabId The ID of the tab being activated. + * @param {String} prevTabId The ID of the previously active tab. + */ +pwlib.appEvent.guiTabActivate = function (tabId, prevTabId) { + /** + * The ID of the tab being activated. + * @type String + */ + this.tabId = tabId; + + /** + * The ID of the previously active tab. + * @type String + */ + this.prevTabId = prevTabId; + + pwlib.appEvent.call(this, 'guiTabActivate', true); +}; + +/** + * @class The color input GUI component. + * + * @private + * + * @param {pwlib.gui} gui Reference to the PaintWeb GUI object. + * + * @param {Element} input Reference to the DOM input element. This can be + * a span, a div, or any other tag. + */ +pwlib.guiColorInput = function (gui, input) { + var _self = this, + colormixer = null, + config = gui.app.config, + doc = gui.app.doc, + MathRound = Math.round, + lang = gui.app.lang; + + /** + * Color input ID. The ID is the same as the data-pwColorInput attribute value + * of the DOM input element . + * + * @type String. + */ + this.id = null; + + /** + * The color input element DOM reference. + * + * @type Element + */ + this.input = input; + + /** + * The configuration property to which this color input is attached to. + * @type String + */ + this.configProperty = null; + + /** + * The configuration group to which this color input is attached to. + * @type String + */ + this.configGroup = null; + + /** + * Reference to the configuration object which holds the color input value. + * @type String + */ + this.configGroupRef = null; + + /** + * Holds the current color displayed by the input. + * + * @type Object + */ + this.color = {red: 0, green: 0, blue: 0, alpha: 0}; + + /** + * Initialize the color input functionality. + * @private + */ + function init () { + var cfgAttr = _self.input.getAttribute('data-pwColorInput'), + cfgNoDots = cfgAttr.replace('.', '_'), + cfgArray = cfgAttr.split('.'), + cfgProp = cfgArray.pop(), + cfgGroup = cfgArray.join('.'), + cfgGroupRef = config, + langGroup = lang.inputs, + labelElem = _self.input.parentNode, + anchor = doc.createElement('a'), + color; + + for (var i = 0, n = cfgArray.length; i < n; i++) { + cfgGroupRef = cfgGroupRef[cfgArray[i]]; + langGroup = langGroup[cfgArray[i]]; + } + + _self.configProperty = cfgProp; + _self.configGroup = cfgGroup; + _self.configGroupRef = cfgGroupRef; + + _self.id = cfgNoDots; + + _self.input.className += ' ' + gui.classPrefix + 'colorInput' + + ' ' + gui.classPrefix + _self.id; + + labelElem.replaceChild(doc.createTextNode(langGroup[cfgProp]), + labelElem.firstChild); + + color = _self.configGroupRef[_self.configProperty]; + color = color.replace(/\s+/g, '').replace(/^rgba\(/, '').replace(/\)$/, ''); + color = color.split(','); + _self.color.red = color[0] / 255; + _self.color.green = color[1] / 255; + _self.color.blue = color[2] / 255; + _self.color.alpha = color[3]; + + anchor.style.backgroundColor = 'rgb(' + color[0] + ',' + color[1] + ',' + + color[2] + ')'; + anchor.style.opacity = color[3]; + + anchor.href = '#'; + anchor.title = langGroup[cfgProp + 'Title'] || langGroup[cfgProp]; + anchor.appendChild(doc.createTextNode(lang.inputs.colorInputAnchorContent)); + anchor.addEventListener('click', ev_input_click, false); + + _self.input.replaceChild(anchor, _self.input.firstChild); + }; + + /** + * The click event handler for the color input element. This + * function shows/hides the Color Mixer panel. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function ev_input_click (ev) { + ev.preventDefault(); + + if (!colormixer) { + colormixer = gui.app.extensions.colormixer; + } + + if (!colormixer.targetInput || colormixer.targetInput.id !== _self.id) { + colormixer.show({ + id: _self.id, + configProperty: _self.configProperty, + configGroup: _self.configGroup, + configGroupRef: _self.configGroupRef, + show: colormixer_show, + hide: colormixer_hide + }, _self.color); + + } else { + colormixer.hide(); + } + }; + + /** + * The color mixer show event handler. This function is invoked + * when the color mixer is shown. + * @private + */ + function colormixer_show () { + var classActive = ' ' + gui.classPrefix + 'colorInputActive', + elemActive = _self.input.className.indexOf(classActive) !== -1; + + if (!elemActive) { + _self.input.className += classActive; + } + }; + + /** + * The color mixer hide event handler. This function is invoked + * when the color mixer is hidden. + * @private + */ + function colormixer_hide () { + var classActive = ' ' + gui.classPrefix + 'colorInputActive', + elemActive = _self.input.className.indexOf(classActive) !== -1; + + if (elemActive) { + _self.input.className = _self.input.className.replace(classActive, ''); + } + }; + + /** + * Update color. This method allows the change of the color values associated + * to the current color input. + * + *

This method is used by the color picker tool and by the global GUI + * configChange application event handler. + * + * @param {Object} color The new color values. The object must have four + * properties: red, green, blue and + * alpha. All values must be between 0 and 1. + */ + this.updateColor = function (color) { + var anchor = _self.input.firstChild.style; + + anchor.opacity = color.alpha; + anchor.backgroundColor = 'rgb(' + MathRound(color.red * 255) + ',' + + MathRound(color.green * 255) + ',' + + MathRound(color.blue * 255) + ')'; + _self.color.red = color.red; + _self.color.green = color.green; + _self.color.blue = color.blue; + _self.color.alpha = color.alpha; + }; + + init(); +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/style.css b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/style.css new file mode 100644 index 00000000..6304ccbd --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/interfaces/default/style.css @@ -0,0 +1,1121 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-11-08 13:15:04 +0200 $ + */ + +.paintweb_placeholder { + background: #fff; + color: #000; + display: block !important; + font: 12px sans-serif; + min-width: 535px +} + +.paintweb_placeholder label { cursor: pointer } + +.paintweb_placeholder input[type=text], .paintweb_placeholder input[type=password], .paintweb_placeholder input[type=number] { border: 1px solid #000 } + +.paintweb_placeholder a:focus { outline: 1px solid #888 } + +.paintweb_appTitle { display: none } + +.paintweb_viewport { + background: #999; + border: 1px solid #000; + height: 400px; + min-height: 150px; + min-width: 535px; + overflow: auto; + position: relative +} + +.paintweb_canvasContainer { + background: url('images/checkers-big.png'); + border: 1px solid #000; + overflow: hidden; + position: relative +} + +.paintweb_layerCanvas, .paintweb_bufferCanvas { + cursor: crosshair; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100% +} + +.paintweb_canvasResizer { + background: #000; + bottom: -5px; + cursor: se-resize; + height: 5px; + overflow: hidden; + position: absolute; + right: -5px; + text-indent: 6px; + width: 5px +} + +/* If we use display:none Opera will not properly determine the image + * width/height. */ +.paintweb_insertimgElement { + position: absolute; + visibility: hidden +} + +.paintweb_mousekeysPointer { + background: #000; + border: 1px solid #fff; + height: 4px; + position: absolute; + width: 4px +} + +.paintweb_mousekeysPointer.paintweb_mouseDown { + background: #0ff; + border: 1px solid #000 +} + +.paintweb_selectionMarquee { + border: 3px dashed #333; + outline: 1px dashed #fff; + opacity: 0.7; + position: absolute +} + +.paintweb_tabPanel { + background: #eee; + position: relative; +} + +.paintweb_tab { + position: relative; +} + +.paintweb_tabsList { + background: #d7d7d7; + border-top: 1px solid #9e9e9e; + list-style: none; + margin: 0; + padding: 0 0 6px 0; + clear: both; +} + +.paintweb_tabsList li { + display: inline; +} + +.paintweb_tabsList a { + background: #e4e4e4; + border-bottom: 1px solid #999; + border-right: 1px solid #acacac; + color: #000; + float: left; + padding: 4px 8px; + text-decoration: none +} + +.paintweb_tabActive a { + background: #eee !important; + padding-top: 5px !important; + padding-bottom: 7px !important; + margin-top: -1px +} + +.paintweb_tabsList a:hover, .paintweb_tabsList a:focus { + background: #fff; + color: #000 +} +.paintweb_main_main{ + display: flex; +} + +.paintweb_tools { + border-left: 1px solid #d6d6d6; + border-top: 1px solid #d6d6d6; + list-style: none; + margin: 0; + padding: 0; + display: flex; + border:0px solid blue; +} +/* .clearfix */ +.paintweb_statusbar:after, .paintweb_tools:after, .paintweb_tabsList:after, .paintweb_colormixer_hexalpha:after, .paintweb_main_selection:after { + content: "."; + display: block; + visibility: hidden; + height: 0; + clear: both; +} + +.paintweb_tabPanel_main { +/* border: 1px solid #9e9e9e; + border-width: 1px 1px 0 1px;*/ +} + +.paintweb_tool, .paintweb_command, .paintweb_icon { + /*background: #f2f2f2;*/ + /*border: 1px solid #d6d6d6;*/ +} + +.paintweb_tools li { + border-top: 0; + border-left: 0; + float: left; +} +.paintweb_tools li:not(.paintweb_toolSeparator){ + border:0px solid red; + padding:10px 8px; +} +.paintweb_tool a, .paintweb_command a, .paintweb_icon a { + background-repeat: no-repeat; + color: #000; + display: block; + height: 31px; + overflow: hidden; + text-indent: 33px; + width: 31px; + white-space: nowrap +} + +.paintweb_strokeFillStyles:hover, .paintweb_strokeFillStyles:focus, +.paintweb_tool:hover, .paintweb_tool:focus, +.paintweb_command:hover, .paintweb_command:focus, +.paintweb_icon:hover, .paintweb_icon:focus { background: #fff } + +.paintweb_tool a:hover, .paintweb_tool a:focus, .paintweb_command a:hover, .paintweb_command a:focus, .paintweb_icon a:hover, .paintweb_icon a:focus { + color: #000; + opacity: 0.5 +} + +.paintweb_toolActive, .paintweb_configActive { + background-color: #929292; + color: #fff +} + +.paintweb_tool.paintweb_disabled, .paintweb_command.paintweb_disabled, .paintweb_icon.paintweb_disabled { + background-color: transparent +} + +.paintweb_tool.paintweb_disabled a, .paintweb_command.paintweb_disabled a, .paintweb_icon.paintweb_disabled a { + cursor: default; + /*opacity: 0.4*/ +} + +.paintweb_toolsWrap { + clear: left; + height: 0; +} + +.paintweb_toolSeparator { + /*border: 1px solid #d6d6d6;*/ + cursor: default; + float: left; + height: 31px; + overflow: hidden; + width: 31px; +} + +.paintweb_statusbar { + background: #eee; + border: 1px solid #9e9e9e; + border-width: 0 1px 1px 1px; + position: relative +} + +.paintweb_imageSize { + background: #cbcbcb; + border-right: 1px solid #a3a3a3; + color: #333; + font-size: 0.9em; + height: 100%; + left: 0; + line-height: 26px; + margin: 0; + padding: 0 8px; + position: absolute; + top: 0; + vertical-align: middle +} + +.paintweb_statusMessage { + margin: 0 11em 0 6em; + padding: 5px 0 +} + +.paintweb_statusZoom { + background: #cbcbcb url('icons/zoom.png') no-repeat; + border: 1px solid #a3a3a3; + border-width: 0 1px; + float: right; + font-size: 0.9em; + height: 100%; + line-height: 28px; + margin: 0; + padding: 0 8px 0 31px; + position: absolute; + right: 20px; + top: 0; + vertical-align: middle +} + +.paintweb_imageZoom { width: 4em } + +.paintweb_viewportResizer { + background: #999; + border: 4px solid #fff; + border-width: 4px 0 0 4px; + bottom: 0; + cursor: se-resize; + height: 10px; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + right: 0; + text-indent: 15px; + width: 10px +} + +.paintweb_viewportResizer:hover, .paintweb_viewportResizer:focus { + background: #fff; + border-color: #999 +} + +.paintweb_main_line, +.paintweb_main_line_bcurve, +.paintweb_main_line_ellipse, +.paintweb_main_line_eraser, +.paintweb_main_line_pencil, +.paintweb_main_line_polygon, +.paintweb_main_line_rectangle, +.paintweb_main_line_text, +.paintweb_main_main, +.paintweb_main_selection, +.paintweb_main_shadow, +.paintweb_main_text { /*margin: 5px*/} + +.paintweb_opt_lineWidth, .paintweb_opt_miterLimit { + background: #f2f2f2; + border: 1px solid #d6d6d6; + line-height: 31px; + margin: 0; + padding: 0 0 0 5px; + position: relative; + white-space: nowrap; + width: 11em +} + +.paintweb_opt_miterLimit { border-top: 0 } + +.paintweb_cfg_line_lineWidth, .paintweb_cfg_line_miterLimit { + height: 1.4em; + margin: 0; + padding: 0; + position: absolute; + right: 5px; + top: 5px; + width: 4em +} + +.paintweb_cfg_line_lineCap, .paintweb_cfg_line_lineJoin { + background: #f2f2f2; + border: 1px solid #d6d6d6; + left: 12.5em; + position: absolute; + top: 0; + white-space: nowrap; + padding: 0 96px 0 5px; + line-height: 31px; + width: 5em +} + +.paintweb_cfg_line_lineJoin { top: 32px } + +.paintweb_cfg_line_lineCap p, .paintweb_cfg_line_lineJoin p { + margin: 0; + padding: 0; +} + +.paintweb_cfg_line_lineCap div, .paintweb_cfg_line_lineJoin div { + border-bottom: 0; + border-right: 0; + border-top: 0; + position: absolute; + top: 0 +} + +.paintweb_lineJoin_miter, .paintweb_lineCap_butt { right: 64px } +.paintweb_lineJoin_round, .paintweb_lineCap_square { right: 32px } +.paintweb_lineJoin_bevel, .paintweb_lineCap_round { right: 0 } + +.paintweb_cfg_shapeType { + background: #f2f2f2; + border: 1px solid #d6d6d6; + left: 28em; + position: absolute; + top: 0; + white-space: nowrap +} + +.paintweb_cfg_shapeType p { + line-height: 31px; + margin: 0; + padding: 0 5px +} + +.paintweb_cfg_shapeType p + div { border-left: 0 } + +.paintweb_cfg_shapeType div { + border-bottom: 0; + border-right: 0; + float: left +} + +.paintweb_main_line_bcurve .paintweb_cfg_line_lineCap, +.paintweb_main_line_rectangle .paintweb_cfg_line_lineJoin { + border-top: 0; + left: 0; + padding-right: 0; + position: relative; + top: 0; + width: 14em +} + +.paintweb_main_line_bcurve .paintweb_opt_lineWidth, +.paintweb_main_line_rectangle .paintweb_opt_lineWidth { width: 14em } + +.paintweb_main_line_rectangle .paintweb_cfg_shapeType, +.paintweb_main_line_bcurve .paintweb_cfg_shapeType { left: 15.6em } + +.paintweb_main_line_polygon .paintweb_opt_lineWidth, +.paintweb_main_line_polygon .paintweb_opt_miterLimit { width: 12em } + +.paintweb_main_line_polygon .paintweb_cfg_line_lineCap, +.paintweb_main_line_polygon .paintweb_cfg_line_lineJoin { left: 13.5em } + +.paintweb_main_line_polygon .paintweb_cfg_shapeType { left: 28.2em } + +.paintweb_main_line_text .paintweb_opt_lineWidth, +.paintweb_main_line_ellipse .paintweb_opt_lineWidth { width: 15em } + +.paintweb_main_line_text .paintweb_cfg_shapeType, +.paintweb_main_line_ellipse .paintweb_cfg_shapeType { + border-top: 0; + left: 0; + padding: 0 9em 0 5px; + position: relative; + top: 0; + width: 6em +} + +.paintweb_main_line_text .paintweb_cfg_shapeType p, +.paintweb_main_line_ellipse .paintweb_cfg_shapeType p { padding: 0 } + +.paintweb_main_line_text .paintweb_icon, +.paintweb_main_line_ellipse .paintweb_icon { + border-left: 1px solid #d6d6d6; + border-right: 0; + border-top: 0; + float: none; + position: absolute; + top: 0 +} + +.paintweb_main_line_text .paintweb_shapeType_stroke, +.paintweb_main_line_ellipse .paintweb_shapeType_stroke { right: 0 } + +.paintweb_main_line_text .paintweb_shapeType_fill, +.paintweb_main_line_ellipse .paintweb_shapeType_fill { right: 32px } + +.paintweb_main_line_text .paintweb_shapeType_both, +.paintweb_main_line_ellipse .paintweb_shapeType_both { right: 64px } + + +.paintweb_main_shadow p { + background: #f2f2f2; + border: 1px solid #d6d6d6; + line-height: 31px; + margin: 0; + padding: 0 5px; + position: relative; + vertical-align: middle; + white-space: nowrap; + width: 11em +} + +.paintweb_main_shadow input { vertical-align: middle } + +.paintweb_cfg_shadow_shadowBlur, .paintweb_cfg_shadow_shadowOffsetX, .paintweb_cfg_shadow_shadowOffsetY { + height: 1.5em; + margin: 0; + padding: 0; + position: absolute; + right: 5px; + top: 5px; + width: 4em +} + +.paintweb_opt_shadowOffsetX { + left: 13em; + position: absolute !important; + top: 0 +} + +.paintweb_opt_shadowOffsetY { + left: 13em; + position: absolute !important; + top: 31px +} + +.paintweb_opt_shadowColor { border-top: 0 !important } + +.paintweb_shadow_shadowColor { + position: absolute; + top: 5px; + right: 5px; +} + +.paintweb_opt_shadowBlur { + left: 26em; + position: absolute !important; + top: 0; + width: 9em !important +} + +.paintweb_main_selection .paintweb_command { + float: left; + margin: 0; + padding: 0; + padding:6px; +} + +.paintweb_main_selection .paintweb_cmd_selectionCopy, +.paintweb_main_selection .paintweb_cmd_clipboardPaste { border-left: 0 } + +.paintweb_cmd_selectionCrop { + border-top: 0; + clear: left +} + +.paintweb_cmd_selectionDelete, .paintweb_cmd_selectionFill { + border-left: 0; + border-top: 0 +} + +.paintweb_opt_selectionTransparent, .paintweb_opt_selectionTransform { + /*background: #f2f2f2;*/ + /*border: 1px solid #d6d6d6;*/ + left: 150px; + line-height: 31px; + margin: 0; + padding: 0 5px; + position: absolute; + top: 10px; + vertical-align: middle; + width: 15em; +} + +.paintweb_opt_selectionTransform { + border-top: 0; + top: 40px +} + +.paintweb_opt_fontFamily label, .paintweb_opt_fontSize label { display: none } + +.paintweb_opt_fontFamily, .paintweb_opt_fontSize { +/* background-image: url('icons/i_fontSize.png'); + background-repeat: no-repeat; + background-position-y: 5px; + background-position-x: 8px;*/ + height:40px; + padding:0 10px; + +} +.paintweb_tab02 { + display: flex; + align-items: center; +} +[class^="paintweb_opt_font"]{ + display: flex; + align-items: center; + border-right:1px solid #ccc; +} + +.paintweb_opt_fontFamily, +.paintweb_cfg_text_fontFamily, +.paintweb_cfg_text_fontSize { + width: 100px; +} + +.paintweb_opt_fontSize { + background: none; +} + +/*.paintweb_cfg_text_fontSize { width: 53px; padding: 4px;}*/ + +.paintweb_opt_textString { + background: #f2f2f2; + border: 1px solid #d6d6d6; + height: 55px; + left: 224px; + margin: 0; + padding: 4px 5px; + position: absolute; + top: 0 +} + +.paintweb_textString { + display: block; + height: 32px; + width: 160px +} + +.paintweb_main_text .paintweb_icon { + border-left: 0; + float: left; + padding:0 4px; +} + +.paintweb_cfg_text_bold { + border-left: 1px solid #d6d6d6; + clear: left; + margin-bottom: 5px +} + +.paintweb_cfg_text_textAlign p { + /*border: 1px solid #d6d6d6;*/ + border-left: 0; + float: left; + height: 0; + margin: 0; + overflow: hidden; + /*padding: 31px 0 0 0;*/ + text-indent: 65px; + white-space: nowrap; + width: 64px +} + +.paintweb_floatingPanel { + -moz-border-radius: 0 0 15px 15px; + -moz-box-shadow: rgba(0, 0, 0, 0.2) 5px 5px 10px; + -webkit-border-bottom-left-radius: 15px; + -webkit-border-bottom-right-radius: 15px; + -webkit-box-shadow: rgba(0, 0, 0, 0.2) 5px 5px 10px; + background: #eee; + border: 1px solid #a2a2a2; + position: absolute +} + +.paintweb_floatingPanel_title { + background: #d2cfcb url('images/floatingPanel-title.png') repeat-x; + border-bottom: 1px solid #a2a2a2; + color: #555; + cursor: pointer; + font-size: 1em; + font-weight: bold; + line-height: 20px; + margin: 0; + padding: 0 10px; + vertical-align: middle +} + +.paintweb_floatingPanel_minimize, .paintweb_floatingPanel_restore, .paintweb_floatingPanel_close { + border-left: 1px solid #a2a2a2; + height: 0; + opacity: 0.4; + overflow: hidden; + padding-top: 20px; + position: absolute; + text-indent: 22px; + top: 0; + white-space: nowrap; + width: 21px +} + +.paintweb_floatingPanel_minimize:hover, .paintweb_floatingPanel_restore:hover, .paintweb_floatingPanel_close:hover, +.paintweb_floatingPanel_minimize:focus, .paintweb_floatingPanel_restore:focus, .paintweb_floatingPanel_close:focus { opacity: 1 } + +.paintweb_floatingPanel_minimize { + background: url('images/floatingPanel-minimize.png') no-repeat; + right: 22px +} + +.paintweb_floatingPanel_restore { + background: url('images/floatingPanel-restore.png') no-repeat; + right: 22px +} + +.paintweb_floatingPanel_close { + background: url('images/floatingPanel-close.png') no-repeat; + right: 0 +} + +.paintweb_floatingPanel_minimized { + height: 1.3em !important; + min-height: 0 !important +} + +.paintweb_floatingPanel_minimized .paintweb_floatingPanel_content, +.paintweb_floatingPanel_minimized .paintweb_floatingPanel_resizer { display: none } + +.paintweb_floatingPanel_content { + margin: 10px; + position: relative +} + +.paintweb_floatingPanel_resizer { + bottom: 0; + cursor: se-resize; + height: 10px; + position: absolute; + right: 0; + width: 10px +} + +.paintweb_floatingPanel_about { + left: 100px; + top: 100px; + width: 400px +} + +.paintweb_floatingPanel_about .paintweb_floatingPanel_content { + background: url('images/paintweb-logo.png') top center no-repeat; + margin-top: 10px; + padding-top: 96px +} + +.paintweb_floatingPanel_colormixer { + height: 275px; + left: 150px; + top: 150px; + width: 570px +} + +.paintweb_colorInput { + background: url('images/checkers.png'); + border: 1px solid #000; + display: block; + height: 20px; + /* + * Opera has problems rendering outlines... + * outline: 1px solid #fff; + */ + width: 40px +} + +.paintweb_colorInputActive { outline: 2px solid #99f } + +.paintweb_colorInput a { + display: block; + height: 100%; + overflow: hidden; + text-decoration: none; + text-indent: 110%; + white-space: nowrap; + width: 100% +} + +.paintweb_buttonStyle button { + width: 100%; + height: 100%; + min-width: 70px; +} + +.paintweb_strokeFillStyles { + /*background: url("images/toolbar-colors.png") no-repeat;*/ + /*font-size: 0.9em;*/ + + /*left: 297px;*/ + /*right: 337px; + height: 67px; + position: absolute;*/ + + width: 40px +} + +.paintweb_opt_fillStyle { + margin: 0 0 0 4px; + padding: 0 +} + +.paintweb_opt_strokeStyle { + margin: -10px 0 0 14px; + padding: 0 0 0 0; + text-align: right +} + +.paintweb_fillStyle { + height: 18px; + width: 18px; +} + +.paintweb_strokeStyle { + height: 18px; + width: 18px; +} + +.paintweb_floatingPanel_colormixer .paintweb_tabsList { + border: 1px solid #9e9e9e; + padding-bottom: 0 +} + +.paintweb_floatingPanel_colormixer .paintweb_tabsList a { + border-bottom: 0; + padding-bottom: 5px; + padding-top: 5px +} + +.paintweb_floatingPanel_colormixer .paintweb_tabActive a { + background: #f2f2f2 !important; + padding-top: 6px !important +} + +.paintweb_floatingPanel_colormixer .paintweb_floatingPanel_content { height: 229px } + +.paintweb_tabPanel_colormixer_selector { + bottom: 0; + left: 0; + position: absolute +} + +.paintweb_tabPanel_colormixer_selector .paintweb_tab { + background: #656565; + border: 1px solid #f2f2f2; + height: 195px; + padding: 2px; + width: 199px +} + +.paintweb_tabPanel_colormixer_inputs { + bottom: 0; + left: 214px; + margin: 0; + position: absolute; + width: 236px +} + +.paintweb_tabPanel_colormixer_inputs ol { + border-width: 0 1px; + border: 1px solid #c6c6c6; + list-style: none; + margin: 0; + padding: 0 +} + +.paintweb_tabPanel_colormixer_inputs ol li { + background: #f2f2f2; + border-top: 1px solid #c6c6c6; + margin: 0; + padding: 7px; + vertical-align: middle +} + +.paintweb_tabPanel_colormixer_inputs input:not([type=radio]) { + margin-top: -2px; + position: absolute; + right: 7px; + text-align: right; + width: 4em +} + +.paintweb_tabPanel_colormixer_inputs input[type=radio] { margin: 0 } + +.paintweb_colormixer_controls { + left: 2px; + position: absolute; + top: 2px +} + +.paintweb_colormixer_chartDot { + background: #000; + border: 1px solid #fff; + height: 3px; + margin: -3px 0 0 -3px; + opacity: 0.5; + position: absolute; + width: 3px +} + +.paintweb_colormixer_slider { + background: #000; + border: 1px solid #fff; + height: 3px; + margin: -2px 0 0 -1px; + opacity: 0.5; + position: absolute +} + +.paintweb_colormixer_cpaletteInput { + margin-bottom: 3px; + width: 97% +} + +.paintweb_colormixer_cpaletteOutput { + height: 166px; + overflow: auto +} + +.paintweb_colormixer_cpaletteOutput a { + float: left; + height: 0; + margin: 1px 1px 0 0; + overflow: hidden; + padding-top: 11px; + text-decoration: none; + width: 11px +} + +.paintweb_colormixer_cpaletteOutput a:hover, +.paintweb_colormixer_cpaletteOutput a:focus { outline: 1px solid #fff } + +.paintweb_colormixer_preview { + background: #f2f2f2; + border: 1px solid #c6c6c6; + color: #555; + font-size: 0.9em; + left: 215px; + list-style: none; + margin: 0; + padding: 0 0 1.6em 0; + position: absolute; + top: 0 +} + +.paintweb_colormixer_preview li { + background: url('images/checkers.png'); + border: 1px solid #000; + float: left; + height: 40px +} + +.paintweb_colormixer_preview span { + display: block; + height: 40px; + margin-bottom: 3px; + width: 40px +} + +.paintweb_colormixer_colorOld { + border-left: 0 !important; + text-align: right +} + +.paintweb_colormixer_colorOld span { cursor: pointer } + +.paintweb_colormixer_actions { + border-bottom: 0; + border: 1px solid #c6c6c6; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + right: 0; + top: 0; + width: 90px +} + +.paintweb_colormixer_actions a { + background: #e7e1db url('images/button.png') repeat-x; + border-bottom: 1px solid #c6c6c6; + color: #666; + display: block; + padding: 2px 5px; + text-decoration: none +} + +.paintweb_colormixer_actions a:hover, .paintweb_colormixer_actions a:focus { + border-color: #000; + color: #000 +} + +.paintweb_colormixer_hexalpha { + background: #f2f2f2; + border: 1px solid #c6c6c6; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + right: 100px; + top: 0 +} + +.paintweb_colormixer_hexalpha li { + float: left; + margin: 5px +} + +.paintweb_colormixer_hexalpha input { display: block } + +.paintweb_ckey_alpha { width: 4em } + +.paintweb_ckey_hex { width: 5em } + + +/* Icons */ + +.paintweb_cmd_historyUndo a { background-image: url('icons/i_cancel.png') } + +.paintweb_cmd_historyRedo a { background-image: url('icons/i_replay.png') } + +.paintweb_cmd_selectionCut a { background-image: url('icons/i_cut.png') } + +.paintweb_cmd_selectionCopy a { background-image: url('icons/i_copy.png') } + +.paintweb_cmd_clipboardPaste a { background-image: url('icons/i_paste.png') } + +.paintweb_cmd_selectionCrop a { background-image: url('icons/i_crop.png') } + +.paintweb_cmd_selectionDelete a { background-image: url('icons/i_block.png') } + +.paintweb_cmd_selectionFill a { background-image: url('icons/i_colorfill.png') } + +.paintweb_cmd_imageSave a { background-image: url('icons/i_save.png') } + +.paintweb_cmd_imageRotate a { background-image: url('icons/i_rotation.png') } + +.paintweb_cmd_imageClear a { background-image: url('icons/i_delete.png') } + +.paintweb_cmd_about { + position: absolute; + top: 5px; + right: 5px; + margin: 0 +} + +.paintweb_cmd_about a { background-image: url('icons/paintweb.png') } + +.paintweb_tool_cbucket a { background-image: url('icons/cbucket.png') } + +.paintweb_tool_cpicker a { background-image: url('icons/cpicker.png') } + +.paintweb_tool_bcurve a { background-image: url('icons/bcurve.png') } + +.paintweb_tool_ellipse a { background-image: url('icons/ellipse.png') } + +.paintweb_tool_line a { background-image: url('icons/line.png') } + +.paintweb_tool_pencil a { background-image: url('icons/pencil.png') } + +.paintweb_tool_hand a { background-image: url('icons/i_pinch.png') } + +.paintweb_tool_polygon a { background-image: url('icons/polygon.png') } + +.paintweb_tool_rectangle a { background-image: url('icons/i_square.png') } + +.paintweb_tool_selection a { background-image: url('icons/i_select.png') } + +.paintweb_tool_text a { background-image: url('icons/i_font.png') } + +.paintweb_tool_insertimg a { background-image: url('icons/i_addPhoto.png') } + +.paintweb_tool_eraser a { background-image: url('icons/eraser.png') } + +.paintweb_lineCap_butt a { background-image: url('icons/line-cap-butt.png') } + +.paintweb_lineCap_square a { background-image: url('icons/line-cap-square.png') } + +.paintweb_lineCap_round a { background-image: url('icons/line-cap-round.png') } + +.paintweb_lineJoin_miter a { background-image: url('icons/line-join-miter.png') } + +.paintweb_lineJoin_round a { background-image: url('icons/line-join-round.png') } + +.paintweb_lineJoin_bevel a { background-image: url('icons/line-join-bevel.png') } + +.paintweb_shapeType_both a { background-image: url('icons/shapeType-both.png') } + +.paintweb_shapeType_fill a { background-image: url('icons/shapeType-fill.png') } + +.paintweb_shapeType_stroke a { background-image: url('icons/shapeType-stroke.png') } + +.paintweb_cfg_text_bold a { background-image: url('icons/i_fontBold.png') } + +.paintweb_cfg_text_italic a { background-image: url('icons/i_fontItalic.png') } + +.paintweb_cfg_text_left a { background-image: url('icons/i_leftAlign.png') } + +.paintweb_cfg_text_center a { background-image: url('icons/i_centerAlign.png') } + +.paintweb_cfg_text_right a { background-image: url('icons/i_rightAlign.png') } + +/* TinyMCE plugin */ + +.paintweb_tinymce_status { + background: #ffffc5; + border-bottom: 0; + border: 1px solid #9e9e9e; + color: #000; + display: block !important; + font: 14px sans-serif; + min-width: 535px; + position: relative +} + +.paintweb_tinymce_status span { + display: block; + margin-right: 9em; + padding: 8px +} + +.paintweb_tinymce_save, .paintweb_tinymce_cancel { + cursor: pointer; + position: absolute; + top: 4px +} + +.paintweb_tinymce_cancel { right: 5px } +.paintweb_tinymce_save { right: 6em } + +/* vim:set fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: */ + + +.inputText { + display:none; + border: 1px dashed blue; + position: absolute; + min-height: 25px; + min-width: 100px; + background-color: transparent; +} + +.progressBar { + width: 200px; + height: 20px; + position: absolute; + top: 200px; + left: 200px; + border:2px solid skyblue; + z-index:99; + border-radius: 25px; +} + +.progressRate{ + width: 0%; + height: 92%; + background-color:#397ca8; + color: white; + font-weight: bold; + text-align: center; + padding-top: 2px; + border-radius: 25px; +} + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/lang/en.json b/src/main/webapp/resources/lib/fims/biz/paintweb/lang/en.json new file mode 100644 index 00000000..fd4d6c38 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/lang/en.json @@ -0,0 +1,268 @@ +{ + // $Date: 2009-11-16 18:13:48 +0200 $ + + "errorInitBufferCanvas": "Error: adding the new buffer canvas element failed.", + "errorInitContext": "Error while initializing the canvas context.", + "errorElementNotFound": "Error: the following element was not found: {id}.", + "noComputedStyle": "Error: window.getComputedStyle is not available.", + "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.", + "errorInitCanvas": "Error: Canvas initialization failed.", + "noCanvasSupport": "Error: Your browser does not support Canvas.", + "failedConfigLoad": "Error: Failed loading the configuration.", + "failedLangLoad": "Error: Failed loading the language file.", + "failedMarkupLoad": "Error: Failed loading the interface markup file.", + "errorInitCommands": "Error: failed to initialize the PaintWeb commands!", + "noToolConfigured": "Error: you have no drawing tool configured to load!", + "imageLoadDifferentHost": "Warning: the configured image cannot be loaded because it is from a different domain.", + "toolRegisterFailed": "Error: failed to register tool '{id}'!", + "extensionRegisterFailed": "Error: failed to register extension '{id}'!", + "errorToolActivate": "Error: the tool you want could not be properly activated!", + "errorInitGUI": "Error: the interface failed to initialize!", + "failedSelectionCopy": "Error: failed to copy the selected pixels into memory.", + "noMainTabPanel": "Error: the interface layout has no tabbed panel with ID = main.", + "guiMarkupImportFailed": "Error: the interface markup code could not be imported into the main document.", + "guiMarkupParseFailed": "Error: the interface markup code could not be properly parsed.", + "missingViewport": "Error: the interface markup does not have the image viewport element.", + "missingViewportResizer": "Error: the interface markup does not have the image viewport resize handle.", + "missingCanvasResizer": "Error: the interface markup does not have the Canvas resize handle.", + "missingCanvasContainer": "Error: the interface markup does not have the Canvas container.", + "errorCpickerUnsupported": "Error: your browser does not implement the get/putImageData methods! The color picker tool cannot be used.", + "errorCbucketUnsupported": "Error: your browser does not implement the get/putImageData methods! The color bucket tool cannot be used.", + "errorClipboardUnsupported": "Error: your browser does not support get/putImageData! Clipboard operations like cut/copy/paste cannot be used.", + "errorTextUnsupported": "Error: your browser does not implement the Canvas Text API! The text tool cannot be used.", + "errorInsertimg": "The image could not be inserted. Maybe the address does not point to an image.", + "errorInsertimgHost": "The URL you provided points to a different host. The image cannot be added for security reasons.", + "errorInsertimgNotLoaded": "The image did not load yet, or the URL you provided does not point to an image.", + "promptInsertimg": "Type the address of the image you want to insert:", + "promptImageDimensions": "Please input the new image dimensions you want.", + "promptTextFont": "Type the name of the font you want:", + "errorImageSave": "The image cannot be saved!", + + "guiCanvasResizer": "Resize the image canvas.", + "guiViewportResizer": "Resize the image viewport.", + "imageZoomTitle": "Zoom image (Use Up/Down Arrow key)", + "imageZoomLabel": "Zoom:", + + "tabs": { + "main": { + "bcurve": "Bézier curve", + "ellipse": "Ellipse", + "eraser": "Eraser", + "line": "Line", + "main": "Main", + "pencil": "Pencil", + "polygon": "Polygon", + "rectangle": "Rectangle", + "selection": "Selection", + "shadow": "Shadow", + "text": "Text", + "textBorder": "Border" + }, + "colormixer_inputs": { + "rgb": "RGB", + "rgbTitle": "sRGB: Standard Red, Green and Blue", + "hsv": "HSV", + "hsvTitle": "Hue, Saturation and Value", + "lab": "Lab", + "labTitle": "CIE Lab: Standard observer 2° D65", + "cmyk": "CMYK", + "cmykTitle": "Cyan, Magenta, Yellow and Key (Black)" + }, + "colormixer_selector": { + "mixer": "Mixer", + "mixerTitle": "Color space visualisation", + "cpalettes": "Palettes", + "cpalettesTitle": "Predefined color palettes" + } + }, + + "floatingPanelMinimize": "Minimize", + "floatingPanelRestore": "Restore", + "floatingPanelClose": "Close", + "floatingPanels": { + "about": "About PaintWeb", + "colormixer": "Color mixer" + }, + + "tools": { + "cbucket": "Color bucket", + "cpicker": "Color picker", + "bcurve": "Bézier curve", + "hand": "Viewport drag", + "ellipse": "Ellipse", + "eraser": "Eraser", + "insertimg": "Insert image", + "line": "Line", + "pencil": "Pencil", + "polygon": "Polygon", + "rectangle": "Rectangle", + "selection": "Rectangle selection", + "text": "Text", + "textUnsupported": "The text tool is not supported by your browser." + }, + + "commands": { + "about": "About PaintWeb", + "clipboardPaste": "Paste clipboard", + "historyRedo": "Redo", + "historyUndo": "Undo", + "imageClear": "Clear image", + "imageSave": "Save image", + "imageRotate": "Rotate image", + "imageBright": "brighten image", + "imageDark": "darken image", + "selectionCopy": "Copy selection", + "selectionCrop": "Crop selection", + "selectionCut": "Cut selection", + "selectionDelete": "Delete selection", + "selectionFill": "Fill the selection" + }, + + "inputs": { + "line": { + "lineCap": "Line cap", + "lineCap_butt": "Butt", + "lineCap_round": "Round", + "lineCap_square": "Square", + "lineJoin": "Line join", + "lineJoin_bevel": "Bevel", + "lineJoin_miter": "Miter", + "lineJoin_round": "Round", + "lineWidth": "Line width:", + "miterLimit": "Miter limit:" + }, + "shadow": { + "enable": "Enable shadows", + "enableTitle": "If checked, a shadow will render after each drawing operation you do.", + "shadowBlur": "Blur:", + "shadowOffsetX": "Distance X:", + "shadowOffsetY": "Distance Y:", + "shadowColor": "Color: ", + "shadowColorTitle": "Shadow color" + }, + "selection": { + "transform": "Image manipulation", + "transformTitle": "If checked, the selected pixels will also be dragged/resized when you make changes to the selection. If unchecked, only the selection marquee will be dragged/resized - pixels will remain unaffected by any such changes.", + "transparent": "Transparent selection", + "transparentTitle": "If checked, the background will remain transparent. If unchecked, the background will be filled with the current fill color." + }, + "text": { + "bold": "Bold", + "italic": "Italic", + "fontFamily": "Font family:", + "fontFamily_add": "Another font...", + "fontSize": "Font size:", + "textAlign": "Text alignment", + "left": "Left", + "center": "Center", + "right": "Right", + "textString_value": "Hello world!" + }, + "shapeType": "Shape type", + "shapeType_both": "Both", + "shapeType_fill": "Fill", + "shapeType_stroke": "Stroke", + "pencilSize": "Pencil size:", + "eraserSize": "Eraser size:", + "borderWidth": "Border width:", + "fillStyle": "Fill ", + "fillStyleTitle": "Fill color", + "strokeStyle": "Stroke ", + "strokeStyleTitle": "Stroke color", + "colorInputAnchorContent": "Click to pick color" + }, + + "colormixer": { + "failedColorPaletteLoad": "Error: failed to load the color palette.", + "colorPalettes": { + "_saved": "Saved colors", + "anpa": "ANPA", + "dic": "DIC Color Guide", + "macos": "Mac OS", + "pantone-solid-coated": "PANTONE solid coated", + "toyo94": "TOYO 94 color finder", + "trumatch": "TRUMATCH colors", + "web": "Web safe", + "windows": "Windows" + }, + "inputs": { + "hex": "Hex", + "alpha": "Alpha", + "hsv_hue": "Hue", + "hsv_sat": "Saturation", + "hsv_val": "Value", + "rgb_red": "Red", + "rgb_green": "Green", + "rgb_blue": "Blue", + "lab_cie_l": "Lightness", + "lab_cie_a": "a*", + "lab_cie_b": "b*", + "cmyk_cyan": "Cyan", + "cmyk_magenta": "Magenta", + "cmyk_yellow": "Yellow", + "cmyk_black": "Key / Black" + }, + "buttons": { + "accept": "Accept", + "cancel": "Cancel", + "saveColor": "Save color", + "pickColor": "Pick color" + } + }, + + "status": { + "cbucketActive": "Click to start flood filling with the current fill color. Right click to use the stroke color for filling.", + "cpickerNormal": "Click to change the fill color, or Shift+Click to change the stroke color.", + "cpicker_fillStyle": "Click to pick the fill color.", + "cpicker_strokeStyle": "Click to pick the stroke color.", + "cpicker_shadow_shadowColor": "Click to pick the shadow color.", + "bcurveActive": "Click to start drawing the curve. You need four points: start, end and two control points.", + "bcurveControlPoint1": "Click to draw the first control point.", + "bcurveControlPoint2": "Click to draw the second control point. This will also end the drawing operation.", + "bcurveSnapping": "Hold the Shift key down for vertical/horizontal snapping.", + "handActive": "Click and drag the image to scroll.", + "ellipseActive": "Click and drag to draw an ellipse.", + "ellipseMousedown": "Hold the Shift key down to draw a circle.", + "eraserActive": "Click and drag to erase.", + "insertimgActive": "Waiting for the image to load...", + "insertimgLoaded": "Pick where you want to place the image. Click and drag to resize the image.", + "insertimgResize": "Hold the Shift key down to preserve the aspect ratio.", + "lineActive": "Click anywhere to start drawing a line.", + "lineMousedown": "Hold the Shift key down for vertical/horizontal snapping.", + "pencilActive": "Click and drag to draw.", + "polygonActive": "Click anywhere to start drawing a polygon.", + "polygonAddPoint": "Click to add another point to the polygon.", + "polygonEnd": "To end drawing the polygon simply click in the same place as the last point.", + "polygonMousedown": "Hold the Shift key down for vertical/horizontal snapping.", + "rectangleActive": "Click and drag to draw a rectangle.", + "rectangleMousedown": "Hold the Shift key down to draw a square.", + "selectionActive": "Click and drag to draw a selection.", + "selectionAvailable": "Drag or resize the selection. Hold the Control key down to toggle the transformation mode.", + "selectionDrag": "Hold the Shift key down for vertical/horizontal snapping.", + "selectionDraw": "Hold the Shift key down to draw a square selection.", + "selectionResize": "Hold the Shift key down to preserve the aspect ratio.", + "textActive": "Pick where you want to place the text. Make sure you adjust the properties as desired.", + "guiCanvasResizerActive": "Move the mouse to resize the image canvas." + }, + + // Moodle-related language strings + "moodle": { + "xhrRequestFailed": "The image save request failed.", + "jsonParseFailed": "Parsing the JSON result from the server failed!", + "imageSaveFailed": "The image save operation failed.", + "urlMismatch": "Image address mismatch!\nThe current image is {url}.\nThe server replied a successful save for {urlServer}.", + "errorSubmitUnsaved": "This image is not saved!" + }, + "moodleServer": { + "permissionDenied": "Permission denied.", + "saveEmptyDataUrl": "Your request has no data URL.", + "proxyNotFound": "Could not find the PaintWeb image file proxy script.", + "malformedDataUrl": "The data URL is malformed.", + "failedMkdir": "Failed to create the PaintWeb images folder inside the Moodle data folder.", + "saveFailed": "Saving the image failed.", + "backingupImages": "Backing-up images saved with PaintWeb...", + "backupFailed": "An error occurred while copying images saved by PaintWeb." + } + + // vim:set spell spl=en fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix ft=javascript: +} diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/lang/ko.json b/src/main/webapp/resources/lib/fims/biz/paintweb/lang/ko.json new file mode 100644 index 00000000..64d91d5e --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/lang/ko.json @@ -0,0 +1,268 @@ +{ + // $Date: 2009-11-16 18:13:48 +0200 $ + + "errorInitBufferCanvas": "Error: adding the new buffer canvas element failed.", + "errorInitContext": "Error while initializing the canvas context.", + "errorElementNotFound": "Error: the following element was not found: {id}.", + "noComputedStyle": "Error: window.getComputedStyle is not available.", + "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.", + "errorInitCanvas": "Error: Canvas initialization failed.", + "noCanvasSupport": "Error: Your browser does not support Canvas.", + "failedConfigLoad": "Error: Failed loading the configuration.", + "failedLangLoad": "Error: Failed loading the language file.", + "failedMarkupLoad": "Error: Failed loading the interface markup file.", + "errorInitCommands": "Error: failed to initialize the PaintWeb commands!", + "noToolConfigured": "Error: you have no drawing tool configured to load!", + "imageLoadDifferentHost": "Warning: the configured image cannot be loaded because it is from a different domain.", + "toolRegisterFailed": "Error: failed to register tool '{id}'!", + "extensionRegisterFailed": "Error: failed to register extension '{id}'!", + "errorToolActivate": "Error: the tool you want could not be properly activated!", + "errorInitGUI": "Error: the interface failed to initialize!", + "failedSelectionCopy": "Error: failed to copy the selected pixels into memory.", + "noMainTabPanel": "Error: the interface layout has no tabbed panel with ID = main.", + "guiMarkupImportFailed": "Error: the interface markup code could not be imported into the main document.", + "guiMarkupParseFailed": "Error: the interface markup code could not be properly parsed.", + "missingViewport": "Error: the interface markup does not have the image viewport element.", + "missingViewportResizer": "Error: the interface markup does not have the image viewport resize handle.", + "missingCanvasResizer": "Error: the interface markup does not have the Canvas resize handle.", + "missingCanvasContainer": "Error: the interface markup does not have the Canvas container.", + "errorCpickerUnsupported": "Error: your browser does not implement the get/putImageData methods! The color picker tool cannot be used.", + "errorCbucketUnsupported": "Error: your browser does not implement the get/putImageData methods! The color bucket tool cannot be used.", + "errorClipboardUnsupported": "Error: your browser does not support get/putImageData! Clipboard operations like cut/copy/paste cannot be used.", + "errorTextUnsupported": "Error: your browser does not implement the Canvas Text API! The text tool cannot be used.", + "errorInsertimg": "The image could not be inserted. Maybe the address does not point to an image.", + "errorInsertimgHost": "The URL you provided points to a different host. The image cannot be added for security reasons.", + "errorInsertimgNotLoaded": "The image did not load yet, or the URL you provided does not point to an image.", + "promptInsertimg": "Type the address of the image you want to insert:", + "promptImageDimensions": "Please input the new image dimensions you want.", + "promptTextFont": "Type the name of the font you want:", + "errorImageSave": "The image cannot be saved!", + + "guiCanvasResizer": "이미지 캔버스의 크기 조정.", + "guiViewportResizer": "Resize the image viewport.", + "imageZoomTitle": "이미지 확대/축소(화살표키를 이용하세요)", + "imageZoomLabel": "Zoom:", + + "tabs": { + "main": { + "bcurve": "베지어 곡선", + "ellipse": "타원", + "eraser": "지우개", + "line": "라인", + "main": "메인", + "pencil": "연필", + "polygon": "다각형", + "rectangle": "사각형", + "selection": "선택", + "shadow": "그림자", + "text": "텍스트", + "textBorder": "외곽선" + }, + "colormixer_inputs": { + "rgb": "RGB", + "rgbTitle": "sRGB: Standard Red, Green and Blue", + "hsv": "HSV", + "hsvTitle": "Hue, Saturation and Value", + "lab": "Lab", + "labTitle": "CIE Lab: Standard observer 2° D65", + "cmyk": "CMYK", + "cmykTitle": "Cyan, Magenta, Yellow and Key (Black)" + }, + "colormixer_selector": { + "mixer": "믹서", + "mixerTitle": "색 영역 시각화", + "cpalettes": "팔레트", + "cpalettesTitle": "미리 정의된 색상 팔레트" + } + }, + + "floatingPanelMinimize": "Minimize", + "floatingPanelRestore": "Restore", + "floatingPanelClose": "Close", + "floatingPanels": { + "about": "About PaintWeb", + "colormixer": "색상" + }, + + "tools": { + "cbucket": "채우기", + "cpicker": "색 선택", + "bcurve": "베지어 곡선", + "hand": "작업영역 움직이기", + "ellipse": "타원", + "eraser": "지우개", + "insertimg": "이미지 추가", + "line": "선 그리기", + "pencil": "연필", + "polygon": "다각형", + "rectangle": "사각형", + "selection": "영역 선택", + "text": "문자열", + "textUnsupported": "텍스트 도구는 브라우저에서 지원되지 않습니다." + }, + + "commands": { + "about": "About PaintWeb", + "clipboardPaste": "붙여넣기", + "historyRedo": "다시 실행", + "historyUndo": "실행취소", + "imageClear": "Clear image", + "imageSave": "이미지 저장", + "imageRotate": "이미지 회전", + "imageBright": "밝게", + "imageDark": "어둡게", + "selectionCopy": "선택영역 복사", + "selectionCrop": "선택영역만 자르기", + "selectionCut": "선택영역 잘라내기", + "selectionDelete": "선택영역 삭제", + "selectionFill": "선택영역 채우기" + }, + + "inputs": { + "line": { + "lineCap": "선끝", + "lineCap_butt": "약간둥굴게", + "lineCap_round": "둥굴게", + "lineCap_square": "직각", + "lineJoin": "Line join", + "lineJoin_bevel": "Bevel", + "lineJoin_miter": "Miter", + "lineJoin_round": "Round", + "lineWidth": "선 굵기:", + "miterLimit": "Miter limit:" + }, + "shadow": { + "enable": "그림자 활성화", + "enableTitle": "선택하면 그리기 작업 후에 그림자가 렌더링됩니다.", + "shadowBlur": "Blur:", + "shadowOffsetX": "Distance X:", + "shadowOffsetY": "Distance Y:", + "shadowColor": "Color: ", + "shadowColorTitle": "그림자 색상" + }, + "selection": { + "transform": "이미지 조작", + "transformTitle": "선택하면 선택 항목을 변경할 때 선택한 픽셀도 드래그/크기 조정됩니다. 선택하지 않으면 선택 마키만 드래그/크기 조정됩니다. 픽셀은 이러한 변경 사항에 영향을 받지 않습니다.", + "transparent": "투명한 선택", + "transparentTitle": "선택하면 배경이 투명하게 유지됩니다. 선택하지 않으면 배경이 현재 채우기 색상으로 채워집니다." + }, + "text": { + "bold": "굵게", + "italic": "기울임꼴", + "fontFamily": "Font family:", + "fontFamily_add": "Another font...", + "fontSize": "Font size:", + "textAlign": "Text alignment", + "left": "Left", + "center": "Center", + "right": "Right", + "textString_value": "문자를 입력하세요" + }, + "shapeType": "도형종류", + "shapeType_both": "채우기와 외곽선", + "shapeType_fill": "채우기만", + "shapeType_stroke": "외곽선만", + "pencilSize": "연필 굵기:", + "eraserSize": "지우개 크기:", + "borderWidth": "외곽선 너비:", + "fillStyle": " ", + "fillStyleTitle": "배경색", + "strokeStyle": " ", + "strokeStyleTitle": "전경색", + "colorInputAnchorContent": "클릭하여 색상 선택" + }, + + "colormixer": { + "failedColorPaletteLoad": "Error: 색상 팔레트를 로드하지 못했습니다.", + "colorPalettes": { + "_saved": "Saved colors", + "anpa": "ANPA", + "dic": "DIC Color Guide", + "macos": "Mac OS", + "pantone-solid-coated": "PANTONE solid coated", + "toyo94": "TOYO 94 color finder", + "trumatch": "TRUMATCH colors", + "web": "Web safe", + "windows": "Windows" + }, + "inputs": { + "hex": "Hex", + "alpha": "Alpha", + "hsv_hue": "Hue", + "hsv_sat": "Saturation", + "hsv_val": "Value", + "rgb_red": "Red", + "rgb_green": "Green", + "rgb_blue": "Blue", + "lab_cie_l": "Lightness", + "lab_cie_a": "a*", + "lab_cie_b": "b*", + "cmyk_cyan": "Cyan", + "cmyk_magenta": "Magenta", + "cmyk_yellow": "Yellow", + "cmyk_black": "Key / Black" + }, + "buttons": { + "accept": "선택", + "cancel": "취소", + "saveColor": "Save color", + "pickColor": "Pick color" + } + }, + + "status": { + "cbucketActive": "현재 채우기 색상으로 플러드 채우기를 시작하려면 클릭합니다. 채우기에 획 색상을 사용하려면 마우스 오른쪽 버튼을 클릭합니다.", + "cpickerNormal": "Click to change the fill color, or Shift+Click to change the stroke color.", + "cpicker_fillStyle": "채우기 색상을 선택하려면 클릭합니다.", + "cpicker_strokeStyle": "색상을 선택하려면 클릭합니다.", + "cpicker_shadow_shadowColor": "그림자 색상 선택[클릭]합니다.", + "bcurveActive": "곡선 그리기를 시작하려면 클릭합니다. [네 개의 점 필요: 시작점, 끝점, 두 개의 제어점]", + "bcurveControlPoint1": "클릭하여 첫 번째 제어점을 그립니다.", + "bcurveControlPoint2": "클릭하여 두 번째 제어점을 그립니다. 이렇게 하면 그리기 작업도 종료됩니다.", + "bcurveSnapping": "수직/수평 스냅을 위해 Shift 키를 누르고 있습니다.", + "handActive": "이미지를 클릭하고 드래그하여 스크롤합니다.", + "ellipseActive": "클릭하고 드래그하여 타원을 그립니다.", + "ellipseMousedown": "Shift 키를 누른 상태에서 원을 그립니다.", + "eraserActive": "클릭하고 드래그하여 지웁니다.", + "insertimgActive": "이미지가 로드되기를 기다리는 중...", + "insertimgLoaded": "이미지를 배치할 위치를 선택합니다. 클릭하고 드래그하여 이미지 크기를 조정합니다.", + "insertimgResize": "종횡비를 유지하려면 Shift 키를 누르고 계십시오.", + "lineActive": "아무 곳이나 클릭하여 선 그리기를 시작합니다..", + "lineMousedown": "수직/수평 스냅을 위해 Shift 키를 누르고 있습니다.", + "pencilActive": "클릭하고 드래그하여 그립니다.", + "polygonActive": "Click anywhere to start drawing a polygon.", + "polygonAddPoint": "Click to add another point to the polygon.", + "polygonEnd": "To end drawing the polygon simply click in the same place as the last point.", + "polygonMousedown": "Hold the Shift key down for vertical/horizontal snapping.", + "rectangleActive": "Click and drag to draw a rectangle.", + "rectangleMousedown": "Hold the Shift key down to draw a square.", + "selectionActive": "Click and drag to draw a selection.", + "selectionAvailable": "Drag or resize the selection. Hold the Control key down to toggle the transformation mode.", + "selectionDrag": "Hold the Shift key down for vertical/horizontal snapping.", + "selectionDraw": "Hold the Shift key down to draw a square selection.", + "selectionResize": "Hold the Shift key down to preserve the aspect ratio.", + "textActive": "Pick where you want to place the text. Make sure you adjust the properties as desired.", + "guiCanvasResizerActive": "Move the mouse to resize the image canvas." + }, + + // Moodle-related language strings + "moodle": { + "xhrRequestFailed": "The image save request failed.", + "jsonParseFailed": "Parsing the JSON result from the server failed!", + "imageSaveFailed": "The image save operation failed.", + "urlMismatch": "Image address mismatch!\nThe current image is {url}.\nThe server replied a successful save for {urlServer}.", + "errorSubmitUnsaved": "This image is not saved!" + }, + "moodleServer": { + "permissionDenied": "Permission denied.", + "saveEmptyDataUrl": "Your request has no data URL.", + "proxyNotFound": "Could not find the PaintWeb image file proxy script.", + "malformedDataUrl": "The data URL is malformed.", + "failedMkdir": "Failed to create the PaintWeb images folder inside the Moodle data folder.", + "saveFailed": "Saving the image failed.", + "backingupImages": "Backing-up images saved with PaintWeb...", + "backupFailed": "An error occurred while copying images saved by PaintWeb." + } + + // vim:set spell spl=en fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix ft=javascript: +} diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb-config.json b/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb-config.json new file mode 100644 index 00000000..334d5eeb --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb-config.json @@ -0,0 +1,644 @@ +{ + // $Date: 2009-11-08 19:54:46 +0200 $ + + "languages": { + "ko": { "title": "Korean" } + }, + + "lang": "ko", + + "langFolder": "lang", + + /* + The graphical user interface you want to use. + @type String + @default "default" + */ + "gui": "default", + + /** + * The folder contains all the interfaces. + * + * @type String + * @default "interfaces" + */ + "interfacesFolder": "interfaces", + + /** + * The interface markup file. The file must be an XHTML valid document. + * + * @type String + * @default "layout.xhtml" + */ + "guiMarkup": "layout.xhtml", + + /** + * The interface style file. + * + * @type String + * @default "style.css" + */ + "guiStyle": "style.css", + + /** + * The interface script file. + * + * @type String + * @default script.js + */ + "guiScript": "script.js", + + /** + * The image viewport width. Make sure the value is a CSS length, like "50%", + * "450px" or "30em". + * + *

Note: the GUI implementation might ignore this value. + * + * @type String + * @default "100%" + */ + "viewportWidth": "100%", + + /** + * The image viewport height. Make sure the value is a CSS length, like "50%", + * "450px" or "30em". + * + *

Note: the GUI implementation might ignore this value. + * + * @type String + * @default "400px" + * 672px + */ + "viewportHeight": "82%", + + /** + * Image save quality for the JPEG format. + * + * @type Number + * @default 0.9 + */ + "jpegSaveQuality": 0.9, + + /** + * The default image width. + * + * @type Number + * @default 400 + * 945 + */ + "imageWidth": 945, + + /** + * The default image width. + * + * @type Number + * @default 300 + */ + "imageHeight": 800, + + /** + * Image preload. The image you want to display when PaintWeb loads. This must + * be a reference to an HTML Image element. + * + * @type HTMLImageElement + * @default null + */ + "imagePreload": null, + + /** + * Default background color. + * + * @type CSS3Color + * @default "#fff" + */ + "backgroundColor": "#fff", + + /** + * Default fill style. + * + * @type CSS3Color-rgba functional notation + * @default "rgba(0,0,0,1)" + */ + "fillStyle": "rgba(0,0,255,1)", + + /** + * Default stroke style. + * + * @type CSS3Color-rgba functional notation + * @default "rgba(0,0,255,1)" + */ + "strokeStyle": "rgba(0,0,0,1)", + + /** + * Enable checkers background. This tells the user interface to render + * checkers in the image background. These are visible only when parts of + * the image being edited are transparent. + * + *

If the device you are running PaintWeb on has limited resources, + * disabling the checkers background should improve the drawing performance. + * + * @type Boolean + * @default true + */ + "checkersBackground": true, + + /** + * GUI placeholder element. This element will hold all the PaintWeb interface + * elements. + * + *

For a successful initialization of PaintWeb, you must define this + * configuration value programatically from your scripts. + * + * @type Element + * @default null + */ + "guiPlaceholder": null, + + /** + * Shape drawing "type": filled, only stroke, or both. Possible values: + * "filled", "stroke" or "both". + * + * @type String + * @default "both" + */ + "shapeType": "both", + + /** + * Number of available history steps. + * + * @type Number + * @default 10 + */ + "historyLimit": 10, + + /** + * Zoom factor when the user increases/decreases the image zoom level. + * + * @type Number + * @default 0.05 + */ + // 0.1 means 10% zoom. it's modified with zoom step in layout.xhtml. + "imageZoomStep": 0.01, + + /** + * The maximum allowed image zoom level. + * + * @type Number + * @default 4 + */ + // 4 means 400% zoom. + "imageZoomMax": 2, + + /** + * The minimum allowed image zoom level. + * + * @type Number + * @default 0.2 + */ + // 0.2 means 20% zoom. + "imageZoomMin": 0.2, + + /** + * The image zoom control keys, for zoom in, zoom out and zoom reset. + * @type Object + */ + "imageZoomKeys": { + "in": "+", + "out": "-", + "reset": "*" + }, + + /** + * Holds the list of drawing tools you want to load. + * @type Array + */ + /*FIXME: 항목수정*/ + /*"tools": ["bcurve", "cbucket", "cpicker", "ellipse", "eraser", "hand", "insertimg", "line", "pencil", "polygon", "rectangle", "selection", "text"],*/ + "tools": ["rectangle", "selection"], + + + /** + * Tools folder. + * @type String + * @default "tools" + */ + "toolsFolder": "tools", + + /** + * The default tool ID. + * + * @type String + * @default "line" + * @see this.tools The array holding the list of drawing tools you want + * loaded. + */ + "toolDefault": "line", + + /** + * Tool drawing delay (milliseconds). Some tools delay their drawing + * operations for performance reasons. + * + * @type Number + * @default 80 + */ + "toolDrawDelay": 80, + + /** + * Holds the list of extensions you want to load. + * @type Array + */ + "extensions": ["colormixer", "mousekeys"], + + /** + * Extensions folder. + * + * @type String + * @default "extensions" + */ + "extensionsFolder": "extensions", + + /** + * @namespace Line tool options. + */ + "line": { + /** + * Line cap. Possible values: "butt", "round", "square". + * + * @type String + * @default "round" + */ + "lineCap": "round", + + /** + * Line join. Possible values: "round", "bevel", "miter". + * + * @type String + * @default "round" + */ + "lineJoin": "round", + + /** + * Line width. + * + * @type Number + * @default 1 + */ + "lineWidth": 1, + + /** + * Miter limit. + * + * @type Number + * @default 10 + */ + "miterLimit": 10 + }, + + /** + * @namespace Shadow options. + */ + "shadow": { + /** + * Tells if a shadow should render or not. + * + * @type Boolean + * @default false + */ + "enable": false, + + /** + * Shadow color + * + * @type CSS3Color-rgba() functional notation + * @default "rgba(0,0,0,1)" + */ + "shadowColor": "rgba(0,0,0,1)", + + /** + * Shadow blur. + * + * @type Number + * @default 5 + */ + "shadowBlur": 5, + + /** + * Shadow offset X. + * + * @type Number + * @default 5 + */ + "shadowOffsetX": 5, + + /** + * Shadow offset %. + * + * @type Number + * @default 5 + */ + "shadowOffsetY": 5 + }, + + /** + * @namespace Selection tool options. + */ + "selection": { + /** + * Selection transformation mode. This tells if any drag/resize would also + * affect the selected pixels or not. + * + * @type Boolean + * @default false + */ + "transform": false, + + /** + * Transparent selection. + * + * @type Boolean + * @default true + */ + "transparent": true, + + /** + * Selection marquee border width. + * + * @type Number + * @default 3 + */ + "borderWidth": 3, + + /** + * Keyboard shortcuts for several selection-related commands. + * @type Object + */ + "keys": { + "selectionCrop": "Control K", + "selectionDelete": "Delete", + "selectionDrop": "Escape", + "selectionFill": "Alt Backspace", + "transformToggle": "Enter" + } + }, + + /** + * Text tool options. + * @type Object + */ + "text": { + "bold": false, + "italic": false, + + + /** + * The default list of font families available in font family drop-down. + * @type Array + */ + "fontFamilies": ["sans-serif", "serif", "cursive", "fantasy", "monospace"], + + /** + * The font family used for rendering the text. + * @type String + * @default "sans-serif" + */ + "fontFamily": "sans-serif", + + "fontSize": 36, + + /** + * Horizontal text alignment. Possible values: "left", "center", "right". + * + *

Note that the Canvas Text API also defines the "start" and "end" + * values, which are not "supported" by PaintWeb. + * + * @type String + * @default "left" + */ + "textAlign": "left", + + /** + * Vertical text alignment. Possible values: "top", "hanging", "middle", + * "alphabetic", "ideographic", or "bottom". + * + * @type String + * @default "alphabetic" + */ + "textBaseline": "top" + }, + + /** + * @namespace Color Mixer extension configuration. + */ + "colormixer": { + /** + * Holds the minimum and maximum value for each color channel input field. + * The value incrementation step is also included - this is used the user + * presses the up/down arrow keys in the input of the color channel. + * + * @type Object + */ + "inputValues": { + // RGB + // order: minimum, maximum, step + "red": [0, 255, 1], + "green": [0, 255, 1], + "blue": [0, 255, 1], + + // HSV + // Hue - degrees + "hue": [0, 360, 1], + "sat": [0, 255, 1], + "val": [0, 255, 1], + + // CMYK - all are percentages + "cyan": [0, 100, 1], + "magenta": [0, 100, 1], + "yellow": [0, 100, 1], + "black": [0, 100, 1], + + // CIE Lab + // cie_l = Lightness, it's a percentage value + // cie_a and cie_b are the color-opponent dimensions + "cie_l": [ 0, 100, 1], + "cie_a": [ -86, 98, 1], + "cie_b": [-107, 94, 1], + + "alpha": [0, 100, 1] + }, + + /** + * CIE Lab configuration. + * @type Object + */ + "lab": { + // The RGB working space is sRGB which has the reference white point of + // D65. + // These are the chromaticity coordinates for the red, green and blue + // primaries. + "x_r": 0.64, + "y_r": 0.33, + "x_g": 0.3, + "y_g": 0.6, + "x_b": 0.13, + "y_b": 0.06, + + // Standard observer: D65 (daylight), 2° (CIE 1931). + // Chromaticity coordinates. + "ref_x": 0.31271, + "ref_y": 0.32902, + + // This is the calculated reference white point (xyY to XYZ) for D65, also + // known as the reference illuminant tristimulus. + // These values are updated based on chromaticity coordinates, during + // initialization. + "w_x": 0.95047, + "w_y": 1, + "w_z": 1.08883, + + // The 3x3 matrix used for multiplying the RGB values when converting RGB + // to XYZ. + // These values are updated based on the chromaticity coordinates, during + // initialization. + "m": [ 0.412424, 0.212656, 0.0193324, + 0.357579, 0.715158, 0.119193, + 0.180464, 0.0721856, 0.950444], + + // The same matrix, but inverted. This is used for the XYZ to RGB conversion. + "m_i": [ 3.24071, -0.969258, 0.0556352, + -1.53726, 1.87599, -0.203996, + -0.498571, 0.0415557, 1.05707] + }, + + /** + * Slider width. This value must be relative to the color space + * visualisation canvas element: 1 means full width, 0.5 means half width, + * etc. + * + * @type Number + * @default 0.10 (which is 10% of the canvas element width) + */ + "sliderWidth": 0.10, + + /** + * Spacing between the slider and the color chart. + * + * @type Number + * @default 0.03 (which is 3% of the canvas element width) + */ + "sliderSpacing": 0.03, + + /** + * Holds the list of color palettes. + * @type Object + */ + "colorPalettes": { + "_saved" : { + // Color values are: red, green, blue. All three channels have values + // ranging between 0 and 1. + "colors" : [[1,1,1], [1,1,0], [1,0,1], [0,1,1], [1,0,0], [0,1,0], [0,0,1], [0,0,0]] + }, + "windows" : { + "file" : "colors/windows.json" + }, + "macos" : { + "file" : "colors/macos.json" + }, + "web" : { + "file" : "colors/web.json" + } + }, + + "paletteDefault": "windows" + }, + + /** + * @namespace Holds the MouseKeys extension options. + */ + "mousekeys": { + /** + * The mouse keys movement acceleration. + * + * @type Number + * @default 0.1 + * @see PaintMouseKeys The MouseKeys extension. + */ + "accel": 0.1, + + /** + * Holds the list of actions, associated to keyboard shortcuts. + * + * @type Object + */ + // We make sure the number keys on the NumPad also work when the Shift key + // is down. + "actions": { + "ButtonToggle": [0], + "SouthWest": [1], + "South": [2], + "SouthEast": [3], + "West": [4], + "ButtonClick": [5], + "East": [6], + "NorthWest": [7], + "North": [8], + "NorthEast": [9] + + /* + You might want Shift+NumPad keys as well ... + Shift+Arrows breaks spatial navigation in Opera. + "ButtonToggle": [0, "Shift Insert"], + "SouthWest": [1, "Shift End"], + "South": [2, "Shift Down"], + "SouthEast": [3, "Shift PageDown"], + "West": [4, "Shift Left"], + "ButtonClick": [5, "Shift Clear"], + "East": [6, "Shift Right"], + "NorthWest": [7, "Shift Home"], + "North": [8, "Shift Up"], + "NorthEast": [9, "Shift PageUp"] + */ + } + }, + + /** + * Keyboard shortcuts associated to drawing tools and other actions. + * + * @type Object + * @see PaintTools The object holding all the drawing tools. + */ + "keys": { + // Use "command": "id" to execute some command. + "Control Z": { "command": "historyUndo" }, + "Control Y": { "command": "historyRedo" }, + + "Control N": { "command": "imageClear" }, + "Control S": { "command": "imageSave" }, + + "Control A": { "command": "selectAll" }, + "Control X": { "command": "selectionCut" }, + "Shift Delete": { "command": "selectionCut" }, + "Control C": { "command": "selectionCopy" }, + "Control V": { "command": "clipboardPaste" }, + + // Use "toolActivate": "id" to activate the tool with the given ID. + "C": { "toolActivate": "cpicker" }, + "E": { "toolActivate": "ellipse" }, + "F": { "toolActivate": "cbucket" }, + "G": { "toolActivate": "polygon" }, + "H": { "toolActivate": "hand" }, + "I": { "toolActivate": "insertimg" }, + "L": { "toolActivate": "line" }, + "O": { "toolActivate": "eraser" }, + "P": { "toolActivate": "pencil" }, + "R": { "toolActivate": "rectangle" }, + "S": { "toolActivate": "selection" }, + "T": { "toolActivate": "text" }, + "V": { "toolActivate": "bcurve" }, + + // Miscellaneous commands. + "X": { "command": "swapFillStroke" }, + "F1": { "command": "about" } + } + + // vim:set spell spl=en fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix ft=javascript: +} diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb.js b/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb.js new file mode 100644 index 00000000..a36d1ecb --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/paintweb.js @@ -0,0 +1,3195 @@ +/* + * Copyright (C) 2008, 2009, 2010 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2010-06-26 22:44:23 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview The main PaintWeb application code. + */ + +/** + * @class The PaintWeb application object. + * + * @param {Window} [win=window] The window object to use. + * @param {Document} [doc=document] The document object to use. + */ +function PaintWeb (win, doc) { + var _self = this; + + if (!win) { + win = window; + } + if (!doc) { + doc = document; + } + + /** + * PaintWeb version. + * @type Number + */ + this.version = 0.9; //! + + /** + * PaintWeb build date (YYYYMMDD). + * @type Number + */ + this.build = -1; //! + + /** + * Holds all the PaintWeb configuration. + * @type Object + */ + this.config = { + showErrors: true + }; + + /** + * Holds all language strings used within PaintWeb. + */ + // Here we include a minimal set of strings, used in case the language file will + // not load. + this.lang = { + "noComputedStyle": "Error: window.getComputedStyle is not available.", + "noXMLHttpRequest": "Error: window.XMLHttpRequest is not available.", + "noCanvasSupport": "Error: Your browser does not support Canvas.", + "guiPlaceholderWrong": "Error: The config.guiPlaceholder property must " + + "reference a DOM element!", + "initHandlerMustBeFunction": "The first argument must be a function.", + "noConfigFile": "Error: You must point to a configuration file by " + + "setting the config.configFile property!", + "failedConfigLoad": "Error: Failed loading the configuration file.", + "failedLangLoad": "Error: Failed loading the language file." + }; + + /** + * Holds the buffer canvas and context references. + * @type Object + */ + this.buffer = {canvas: null, context: null}; + + /** + * Holds the current layer ID, canvas and context references. + * @type Object + */ + this.layer = {id: null, canvas: null, context: null}; + + /** + * 원본 이미지 + */ + this.orginl = {canvas: null, context: null}; + + /** + * The instance of the active tool object. + * + * @type Object + * + * @see PaintWeb.config.toolDefault holds the ID of the tool which is + * activated when the application loads. + * @see PaintWeb#toolActivate Activate a drawing tool by ID. + * @see PaintWeb#toolRegister Register a new drawing tool. + * @see PaintWeb#toolUnregister Unregister a drawing tool. + * @see pwlib.tools holds the drawing tools. + */ + this.tool = null; + + /** + * Holds references to DOM elements. + * + * @private + * @type Object + */ + this.elems = {}; + + /** + * Holds the last recorded mouse coordinates and the button state (if it's + * down or not). + * + * @private + * @type Object + */ + this.mouse = {x: 0, y: 0, buttonDown: false}; + + /** + * Holds all the PaintWeb extensions. + * + * @type Object + * @see PaintWeb#extensionRegister Register a new extension. + * @see PaintWeb#extensionUnregister Unregister an extension. + * @see PaintWeb.config.extensions Holds the list of extensions to be loaded + * automatically when PaintWeb is initialized. + */ + this.extensions = {}; + + /** + * Holds all the PaintWeb commands. Each property in this object must + * reference a simple function which can be executed by keyboard shortcuts + * and/or GUI elements. + * + * @type Object + * @see PaintWeb#commandRegister Register a new command. + * @see PaintWeb#commandUnregister Unregister a command. + */ + this.commands = {}; + + /** + * The graphical user interface object instance. + * @type pwlib.gui + */ + this.gui = null; + + /** + * The document element PaintWeb is working with. + * + * @private + * @type Document + * @default document + */ + this.doc = doc; + + /** + * The window object PaintWeb is working with. + * + * @private + * @type Window + * @default window + */ + this.win = win; + + /** + * 밝기조절 슬라이더 값 + * + */ + this.brightnessSliderValue = 0; + + /** + * Holds image information: width, height, zoom and more. + * + * @type Object + */ + this.image = { + /** + * Image width. + * + * @type Number + */ + width: 0, + + /** + * Image height. + * + * @type Number + */ + height: 0, + + /** + * Image zoom level. This property holds the current image zoom level used + * by the user for viewing the image. + * + * @type Number + * @default 1 + */ + zoom: 1, + + /** + * Image scaling. The canvas elements are scaled from CSS using this value + * as the scaling factor. This value is dependant on the browser rendering + * resolution and on the user-defined image zoom level. + * + * @type Number + * @default 1 + */ + canvasScale: 1, + + /** + * Tells if the current image has been modified since the initial load. + * + * @type Boolean + * @default false + */ + modified: false + }; + + /** + * Resolution information. + * + * @type Object + */ + this.resolution = { + /** + * The DOM element holding information about the current browser rendering + * settings (zoom / DPI). + * + * @private + * @type Element + */ + elem: null, + + /** + * The ID of the DOM element holding information about the current browser + * rendering settings (zoom / DPI). + * + * @private + * @type String + * @default 'paintweb_resInfo' + */ + elemId: 'paintweb_resInfo', + + /** + * The styling necessary for the DOM element. + * + * @private + * @type String + */ + cssText: '@media screen and (resolution:96dpi){' + + '#paintweb_resInfo{width:96px}}' + + '@media screen and (resolution:134dpi){' + + '#paintweb_resInfo{width:134px}}' + + '@media screen and (resolution:200dpi){' + + '#paintweb_resInfo{width:200px}}' + + '@media screen and (resolution:300dpi){' + + '#paintweb_resInfo{width:300px}}' + + '#paintweb_resInfo{' + + 'display:block;' + + 'height:100%;' + + 'left:-3000px;' + + 'position:fixed;' + + 'top:0;' + + 'visibility:hidden;' + + 'z-index:-32}', + + /** + * Optimal DPI for the canvas elements. + * + * @private + * @type Number + * @default 96 + */ + dpiOptimal: 96, + + /** + * The current DPI used by the browser for rendering the entire page. + * + * @type Number + * @default 96 + */ + dpiLocal: 96, + + /** + * The current zoom level used by the browser for rendering the entire page. + * + * @type Number + * @default 1 + */ + browserZoom: 1, + + /** + * The scaling factor used by the browser for rendering the entire page. For + * example, on Gecko using DPI 200 the scale factor is 2. + * + * @private + * @type Number + * @default -1 + */ + scale: -1 + }; + + /** + * The image history. + * + * @private + * @type Object + */ + this.history = { + /** + * History position. + * + * @type Number + * @default 0 + */ + pos: 0, + + /** + * The ImageDatas for each history state. + * + * @private + * @type Array + */ + states: [] + }; + + /** + * Tells if the browser supports the Canvas Shadows API. + * + * @type Boolean + * @default true + */ + this.shadowSupported = true; + + /** + * Tells if the current tool allows the drawing of shadows. + * + * @type Boolean + * @default true + */ + this.shadowAllowed = true; + + /** + * Image in the clipboard. This is used when some selection is copy/pasted. + * + * @type ImageData + */ + this.clipboard = false; + + /** + * Application initialization state. This property can be in one of the + * following states: + * + *

    + *
  • {@link PaintWeb.INIT_NOT_STARTED} - The initialization is not + * started. + * + *
  • {@link PaintWeb.INIT_STARTED} - The initialization process is + * running. + * + *
  • {@link PaintWeb.INIT_DONE} - The initialization process has completed + * successfully. + * + *
  • {@link PaintWeb.INIT_ERROR} - The initialization process has failed. + *
+ * + * @type Number + * @default PaintWeb.INIT_NOT_STARTED + */ + this.initialized = PaintWeb.INIT_NOT_STARTED; + + /** + * Custom application events object. + * + * @type pwlib.appEvents + */ + this.events = null; + + /** + * Unique ID for the current PaintWeb instance. + * + * @type Number + */ + this.UID = 0; + + /** + * List of Canvas context properties to save and restore. + * + *

When the Canvas is resized the state is lost. Using context.save/restore + * state does work only in Opera. In Firefox/Gecko and WebKit saved states are + * lost after resize, so there's no state to restore. As such, PaintWeb has + * its own simple state save/restore mechanism. The property values are saved + * into a JavaScript object. + * + * @private + * @type Array + * + * @see PaintWeb#stateSave to save the canvas context state. + * @see PaintWeb#stateRestore to restore a canvas context state. + */ + this.stateProperties = ['strokeStyle', 'fillStyle', 'globalAlpha', + 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', 'shadowOffsetX', + 'shadowOffsetY', 'shadowBlur', 'shadowColor', 'globalCompositeOperation', + 'font', 'textAlign', 'textBaseline']; + + /** + * Holds the keyboard event listener object. + * + * @private + * @type pwlib.dom.KeyboardEventListener + * @see pwlib.dom.KeyboardEventListener The class dealing with the + * cross-browser differences in the DOM keyboard events. + */ + var kbListener_ = null; + + /** + * Holds temporary state information during PaintWeb initialization. + * + * @private + * @type Object + */ + var temp_ = {onInit: null, toolsLoadQueue: 0, extensionsLoadQueue: 0}; + + // Avoid global scope lookup. + var MathAbs = Math.abs, + MathFloor = Math.floor, + MathMax = Math.max, + MathMin = Math.min, + MathRound = Math.round, + pwlib = null, + appEvent = null, + lang = this.lang; + + /** + * Element node type constant. + * + * @constant + * @type Number + */ + this.ELEMENT_NODE = window.Node ? Node.ELEMENT_NODE : 1; + + /** + * PaintWeb pre-initialization code. This runs when the PaintWeb instance is + * constructed. + * @private + */ + function preInit() { + var d = new Date(); + + // If PaintWeb is running directly from the source code, then the build date + // is always today. + if (_self.build === -1) { + var dateArr = [d.getFullYear(), d.getMonth()+1, d.getDate()]; + + if (dateArr[1] < 10) { + dateArr[1] = '0' + dateArr[1]; + } + if (dateArr[2] < 10) { + dateArr[2] = '0' + dateArr[2]; + } + + _self.build = dateArr.join(''); + } + + _self.UID = d.getMilliseconds() * MathRound(Math.random() * 100); + _self.elems.head = doc.getElementsByTagName('head')[0] || doc.body; + }; + + /** + * Initialize PaintWeb. + * + *

This method is asynchronous, meaning that it will return much sooner + * before the application initialization is completed. + * + * @param {Function} [handler] The appInit event handler. Your + * event handler will be invoked automatically when PaintWeb completes + * loading, or when an error occurs. + * + * @returns {Boolean} True if the initialization has been started + * successfully, or false if not. + */ + this.init = function (handler) { + if (this.initialized === PaintWeb.INIT_DONE) { + return true; + } + + this.initialized = PaintWeb.INIT_STARTED; + + if (handler && typeof handler !== 'function') { + throw new TypeError(lang.initHandlerMustBeFunction); + } + + temp_.onInit = handler; + + // Check Canvas support. + if (!doc.createElement('canvas').getContext) { + this.initError(lang.noCanvasSupport); + return false; + } + + // Basic functionality used within the Web application. + if (!window.getComputedStyle) { + try { + if (!win.getComputedStyle(doc.createElement('div'), null)) { + this.initError(lang.noComputedStyle); + return false; + } + } catch (err) { + this.initError(lang.noComputedStyle); + return false; + } + } + + if (!window.XMLHttpRequest) { + this.initError(lang.noXMLHttpRequest); + return false; + } + + if (!this.config.configFile) { + this.initError(lang.noConfigFile); + return false; + } + + if (typeof this.config.guiPlaceholder !== 'object' || + this.config.guiPlaceholder.nodeType !== this.ELEMENT_NODE) { + this.initError(lang.guiPlaceholderWrong); + return false; + } + + // Silently ignore any wrong value for the config.imageLoad property. + if (typeof this.config.imageLoad !== 'object' || + this.config.imageLoad.nodeType !== this.ELEMENT_NODE) { + this.config.imageLoad = null; + } + + // JSON parser and serializer. + if (!window.JSON) { + this.scriptLoad(PaintWeb.baseFolder + 'includes/json2.js', + this.jsonlibReady); + } else { + this.jsonlibReady(); + } + + return true; + }; + + /** + * The load event handler for the JSON library script. + * @private + */ + this.jsonlibReady = function () { + if (window.pwlib) { + _self.pwlibReady(); + } else { + _self.scriptLoad(PaintWeb.baseFolder + 'includes/lib.js', + _self.pwlibReady); + } + }; + + /** + * The load event handler for the PaintWeb library script. + * @private + */ + this.pwlibReady = function () { + pwlib = window.pwlib; + appEvent = pwlib.appEvent; + + // Create the custom application events object. + _self.events = new pwlib.appEvents(_self); + _self.configLoad(); + }; + + /** + * Report an initialization error. + * + *

This method dispatches the {@link pwlib.appEvent.appInit} event. + * + * @private + * + * @param {String} msg The error message. + * + * @see pwlib.appEvent.appInit + */ + this.initError = function (msg) { + switch (this.initialized) { + case PaintWeb.INIT_ERROR: + case PaintWeb.INIT_DONE: + case PaintWeb.INIT_NOT_STARTED: + return; + } + + this.initialized = PaintWeb.INIT_ERROR; + + var ev = null; + + if (this.events && 'dispatch' in this.events && + appEvent && 'appInit' in appEvent) { + + ev = new appEvent.appInit(this.initialized, msg); + this.events.dispatch(ev); + } + + if (typeof temp_.onInit === 'function') { + if (!ev) { + // fake an event dispatch. + ev = {type: 'appInit', state: this.initialized, errorMessage: msg}; + } + + temp_.onInit.call(this, ev); + } + + if (this.config.showErrors) { + alert(msg); + } else if (window.console && console.log) { + console.log(msg); + } + }; + + /** + * Asynchronously load the configuration file. This method issues an + * XMLHttpRequest to load the JSON file. + * + * @private + * + * @see PaintWeb.config.configFile The configuration file. + * @see pwlib.xhrLoad The library function being used for creating the + * XMLHttpRequest object. + */ + this.configLoad = function () { + pwlib.xhrLoad(PaintWeb.baseFolder + this.config.configFile, + this.configReady); + }; + + /** + * The configuration reader. This is the event handler for the XMLHttpRequest + * object, for the onreadystatechange event. + * + * @private + * + * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. + * + * @see PaintWeb#configLoad The method which issues the XMLHttpRequest request + * for loading the configuration file. + */ + this.configReady = function (xhr) { + /* + * readyState values: + * 0 UNINITIALIZED open() has not been called yet. + * 1 LOADING send() has not been called yet. + * 2 LOADED send() has been called, headers and status are available. + * 3 INTERACTIVE Downloading, responseText holds the partial data. + * 4 COMPLETED Finished with all operations. + */ + if (!xhr || xhr.readyState !== 4) { + return; + } + + if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { + _self.initError(lang.failedConfigLoad); + return; + } + + var config = pwlib.jsonParse(xhr.responseText); + pwlib.extend(_self.config, config); + + _self.langLoad(); + }; + + /** + * Asynchronously load the language file. This method issues an XMLHttpRequest + * to load the JSON file. + * + * @private + * + * @see PaintWeb.config.lang The language you want for the PaintWeb user + * interface. + * @see pwlib.xhrLoad The library function being used for creating the + * XMLHttpRequest object. + */ + this.langLoad = function () { + var id = this.config.lang, + file = PaintWeb.baseFolder; + + // If the language is not available, always fallback to English. + if (!(id in this.config.languages)) { + id = this.config.lang = 'en'; + } + + if ('file' in this.config.languages[id]) { + file += this.config.languages[id].file; + } else { + file += this.config.langFolder + '/' + id + '.json'; + } + + pwlib.xhrLoad(file, this.langReady); + }; + + /** + * The language file reader. This is the event handler for the XMLHttpRequest + * object, for the onreadystatechange event. + * + * @private + * + * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. + * + * @see PaintWeb#langLoad The method which issues the XMLHttpRequest request + * for loading the language file. + */ + this.langReady = function (xhr) { + if (!xhr || xhr.readyState !== 4) { + return; + } + + if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) { + _self.initError(lang.failedLangLoad); + return; + } + + pwlib.extend(_self.lang, pwlib.jsonParse(xhr.responseText)); + + if (_self.initCanvas() && _self.initContext()) { + // Start GUI load now. + _self.guiLoad(); + } else { + _self.initError(lang.errorInitCanvas); + } + }; + + /** + * Initialize the PaintWeb commands. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initCommands = function () { + if (this.commandRegister('historyUndo', this.historyUndo) && + this.commandRegister('historyRedo', this.historyRedo) && + this.commandRegister('selectAll', this.selectAll) && + this.commandRegister('selectionCrop', this.selectionCrop) && + this.commandRegister('selectionFill', this.selectionFill) && + //this.commandRegister('selectionCut', this.selectionCut) && + //this.commandRegister('selectionCopy', this.selectionCopy) && + //this.commandRegister('clipboardPaste', this.clipboardPaste) && + this.commandRegister('imageSave', this.imageSave) && + this.commandRegister('imageRotate', this.imageRotate) && + this.commandRegister('imageBright', this.imageBright) && + this.commandRegister('imageDark', this.imageDark) && + this.commandRegister('imageClear', this.imageClear) && + this.commandRegister('swapFillStroke', this.swapFillStroke) && + this.commandRegister('imageZoomIn', this.imageZoomIn) && + this.commandRegister('imageZoomOut', this.imageZoomOut) && + this.commandRegister('imageZoomReset', this.imageZoomReset)) { + return true; + } else { + this.initError(lang.errorInitCommands); + return false; + } + }; + + /** + * Load th PaintWeb GUI. This method loads the GUI markup file, the stylesheet + * and the script. + * + * @private + * + * @see PaintWeb.config.guiStyle The interface style file. + * @see PaintWeb.config.guiScript The interface script file. + * @see pwlib.gui The interface object. + */ + this.guiLoad = function () { + var cfg = this.config, + gui = this.config.gui, + base = PaintWeb.baseFolder + cfg.interfacesFolder + '/' + gui + '/', + style = base + cfg.guiStyle, + script = base + cfg.guiScript; + + this.styleLoad(gui + 'style', style); + + if (pwlib.gui) { + this.guiScriptReady(); + } else { + this.scriptLoad(script, this.guiScriptReady); + } + }; + + /** + * The load event handler for the PaintWeb GUI script. This + * method creates an instance of the GUI object that just loaded and starts + * loading the GUI markup. + * + * @private + * + * @see PaintWeb.config.guiScript The interface script file. + * @see PaintWeb.config.guiMarkup The interface markup file. + * @see pwlib.gui The interface object. + * @see pwlib.xhrLoad The library function being used for creating the + * XMLHttpRequest object. + */ + this.guiScriptReady = function () { + var cfg = _self.config, + gui = _self.config.gui, + base = cfg.interfacesFolder + '/' + gui + '/', + markup = base + cfg.guiMarkup; + + _self.gui = new pwlib.gui(_self); + + // Check if the interface markup is cached already. + if (markup in pwlib.fileCache) { + if (_self.gui.init(pwlib.fileCache[markup])) { + _self.initTools(); + } else { + _self.initError(lang.errorInitGUI); + } + + } else { + pwlib.xhrLoad(PaintWeb.baseFolder + markup, _self.guiMarkupReady); + } + }; + + /** + * The GUI markup reader. This is the event handler for the XMLHttpRequest + * object, for the onreadystatechange event. + * + * @private + * + * @param {XMLHttpRequest} xhr The XMLHttpRequest object being handled. + * + * @see PaintWeb#guiScriptReady The method which issues the XMLHttpRequest + * request for loading the interface markup file. + */ + this.guiMarkupReady = function (xhr) { + if (!xhr || xhr.readyState !== 4) { + return; + } + + if (xhr.status !== 304 && xhr.status !== 200) { + _self.initError(lang.failedMarkupLoad); + return; + } + + var param; + /*if (xhr.responseXML && xhr.responseXML.documentElement) { // by gu + param = xhr.responseXML; + } else */if (xhr.responseText) { + param = xhr.responseText; + } else { + _self.initError(lang.failedMarkupLoad); + return; + } + + if (_self.gui.init(param)) { + _self.initTools(); + } else { + _self.initError(lang.errorInitGUI); + } + }; + + /** + * Initialize the Canvas elements. This method creates the elements and + * sets-up their dimensions. + * + *

The layer Canvas element will have the background rendered with the + * color from {@link PaintWeb.config.backgroundColor}. + * + *

If {@link PaintWeb.config.imageLoad} is defined, then the image element + * is inserted into the Canvas image. + * + *

All the Canvas event listeners are also attached to the buffer Canvas + * element. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + * + * @see PaintWeb#ev_canvas The global Canvas events handler. + */ + this.initCanvas = function () { + var cfg = this.config, + res = this.resolution, + resInfo = doc.getElementById(res.elemId), + layerCanvas = doc.createElement('canvas'), + bufferCanvas = doc.createElement('canvas'), + orginlCanvas = doc.createElement('canvas'), + layerContext = layerCanvas.getContext('2d'), + bufferContext = bufferCanvas.getContext('2d'), + orginlContext = orginlCanvas.getContext('2d'), + width = cfg.imageWidth, + height = cfg.imageHeight, + imageLoad = cfg.imageLoad; + + if (!resInfo) { + var style = doc.createElement('style'); + style.type = 'text/css'; + style.appendChild(doc.createTextNode(res.cssText)); + _self.elems.head.appendChild(style); + + resInfo = doc.createElement('div'); + resInfo.id = res.elemId; + doc.body.appendChild(resInfo); + } + + if (!resInfo) { + this.initError(lang.errorInitCanvas); + return false; + } + if (!layerCanvas || !bufferCanvas || !layerContext || !bufferContext) { + this.initError(lang.noCanvasSupport); + return false; + } + + if (!pwlib.isSameHost(imageLoad.src, win.location.host)) { + cfg.imageLoad = imageLoad = null; + alert(lang.imageLoadDifferentHost); + } + + if (imageLoad) { + width = parseInt(imageLoad.width); + height = parseInt(imageLoad.height); + } + + res.elem = resInfo; + this.image.width = layerCanvas.width = bufferCanvas.width = orginlCanvas.width = width; + this.image.height = layerCanvas.height = bufferCanvas.height = orginlCanvas.height = height; + + this.layer.canvas = layerCanvas; + this.layer.context = layerContext; + this.buffer.canvas = bufferCanvas; + this.buffer.context = bufferContext; + this.orginl.canvas = orginlCanvas; + this.orginl.context = orginlContext; + + if (imageLoad) { + layerContext.drawImage(imageLoad, 0, 0); + orginlContext.drawImage(imageLoad,0,0); + orginlCanvas.style.display = 'none'; + } else { + // Set the configured background color. + var fillStyle = layerContext.fillStyle; + layerContext.fillStyle = cfg.backgroundColor; + layerContext.fillRect(0, 0, width, height); + layerContext.fillStyle = fillStyle; + } + + /* + * Setup the event listeners for the canvas element. + * + * The event handler (ev_canvas) calls the event handlers associated with + * the active tool (e.g. tool.mousemove). + */ + var events = ['dblclick', 'click', 'mousedown', 'mouseup', 'mousemove', + 'contextmenu'], + n = events.length; + + for (var i = 0; i < n; i++) { + bufferCanvas.addEventListener(events[i], this.ev_canvas, false); + } + + + //--------------------------------------------------------------------------------------- + // FIXME : image size scale 조정 - start + //--------------------------------------------------------------------------------------- + let scaleRate = 1.0; + + let imgWidth = parseInt(imageLoad.width); + let imgHeight = parseInt(imageLoad.height); + const cfgImgWidth = cfg.imageWidth; + const cfgImgHeight = cfg.imageHeight; + const calWidth = imgWidth - cfgImgWidth; + const calHeight = imgHeight -cfgImgHeight; + + + if(calWidth > calHeight) { + if (imgWidth > cfgImgWidth) { + scaleRate = cfgImgWidth / imgWidth; + imgHeight = Math.floor(imgHeight * scaleRate); + } + // viewportHeight": "620px" 설정에 맞게 set + if (imgHeight > cfgImgHeight) { + scaleRate = scaleRate * (cfgImgHeight / imgHeight); + } + }else{ + if (imgHeight > cfgImgHeight) { + scaleRate = cfgImgHeight / imgHeight; + imgWidth = Math.floor(imgWidth * scaleRate); + } + // viewportHeight": "620px" 설정에 맞게 set + if (imgWidth > cfgImgWidth) { + scaleRate = scaleRate * (cfgImgWidth / imgWidth); + } + } + scaleRate = scaleRate.toFixed(2); + console.log('>>>>>>>>initCanvas', imageLoad.width*scaleRate, imageLoad.height*scaleRate, scaleRate) + + this.image.zoom = scaleRate; + //this.imageZoomTo(scaleRate) + //--------------------------------------------------------------------------------------- + // FIXME : image size scale 조정 - end + //--------------------------------------------------------------------------------------- + + + return true; + }; + + /** + * Initialize the Canvas buffer context. This method updates the context + * properties to reflect the values defined in the PaintWeb configuration + * file. + * + *

Shadows support is also determined. The {@link PaintWeb#shadowSupported} + * value is updated accordingly. + * + * @private + * @returns {Boolean} True if the initialization was successful, or false if + * not. + */ + this.initContext = function () { + var bufferContext = this.buffer.context; + + // Opera does not render shadows, at the moment. + if (!pwlib.browser.opera && bufferContext.shadowColor && 'shadowOffsetX' in + bufferContext && 'shadowOffsetY' in bufferContext && 'shadowBlur' in + bufferContext) { + this.shadowSupported = true; + } else { + this.shadowSupported = false; + } + + var cfg = this.config, + props = { + fillStyle: cfg.fillStyle, + font: cfg.text.fontSize + 'px ' + cfg.text.fontFamily, + lineCap: cfg.line.lineCap, + lineJoin: cfg.line.lineJoin, + lineWidth: cfg.line.lineWidth, + miterLimit: cfg.line.miterLimit, + strokeStyle: cfg.strokeStyle, + textAlign: cfg.text.textAlign, + textBaseline: cfg.text.textBaseline + }; + + if (cfg.text.bold) { + props.font = 'bold ' + props.font; + } + + if (cfg.text.italic) { + props.font = 'italic ' + props.font; + } + + // Support Gecko 1.9.0 + if (!bufferContext.fillText && 'mozTextStyle' in bufferContext) { + props.mozTextStyle = props.font; + } + + for (var prop in props) { + bufferContext[prop] = props[prop]; + } + + // shadows are only for the layer context. + if (cfg.shadow.enable && this.shadowSupported) { + var layerContext = this.layer.context; + layerContext.shadowColor = cfg.shadow.shadowColor; + layerContext.shadowBlur = cfg.shadow.shadowBlur; + layerContext.shadowOffsetX = cfg.shadow.shadowOffsetX; + layerContext.shadowOffsetY = cfg.shadow.shadowOffsetY; + } + + return true; + }; + + /** + * Initialization procedure which runs after the configuration, language and + * GUI files have loaded. + * + *

This method dispatches the {@link pwlib.appEvent.appInit} event. + * + * @private + * + * @see pwlib.appEvent.appInit + */ + this.initComplete = function () { + if (!this.initCommands()) { + this.initError(lang.errorInitCommands); + return; + } + + // The initial blank state of the image + this.historyAdd(); + this.image.modified = false; + + // The global keyboard events handler implements everything needed for + // switching between tools and for accessing any other functionality of the + // Web application. + kbListener_ = new pwlib.dom.KeyboardEventListener(this.config.guiPlaceholder, + {keydown: this.ev_keyboard, + keypress: this.ev_keyboard, + keyup: this.ev_keyboard}); + + this.updateCanvasScaling(); + this.win.addEventListener('resize', this.updateCanvasScaling, false); + + this.events.add('configChange', this.configChangeHandler); + this.events.add('imageSaveResult', this.imageSaveResultHandler); + + // Add the init event handler. + if (typeof temp_.onInit === 'function') { + _self.events.add('appInit', temp_.onInit); + delete temp_.onInit; + } + + this.initialized = PaintWeb.INIT_DONE; + + this.events.dispatch(new appEvent.appInit(this.initialized)); + }; + + /** + * Load all the configured drawing tools. + * @private + */ + this.initTools = function () { + var id = '', + cfg = this.config, + n = cfg.tools.length, + base = PaintWeb.baseFolder + cfg.toolsFolder + '/'; + + if (n < 1) { + this.initError(lang.noToolConfigured); + return; + } + + temp_.toolsLoadQueue = n; + + for (var i = 0; i < n; i++) { + id = cfg.tools[i]; + if (id in pwlib.tools) { + this.toolLoaded(); + } else { + this.scriptLoad(base + id + '.js' , this.toolLoaded); + } + } + }; + + /** + * The load event handler for each tool script. + * @private + */ + this.toolLoaded = function () { + temp_.toolsLoadQueue--; + + if (temp_.toolsLoadQueue === 0) { + var t = _self.config.tools, + n = t.length; + + for (var i = 0; i < n; i++) { + if (!_self.toolRegister(t[i])) { + _self.initError(pwlib.strf(lang.toolRegisterFailed, {id: t[i]})); + return; + } + } + + _self.initExtensions(); + } + }; + + /** + * Load all the extensions. + * @private + */ + this.initExtensions = function () { + var id = '', + cfg = this.config, + n = cfg.extensions.length, + base = PaintWeb.baseFolder + cfg.extensionsFolder + '/'; + + if (n < 1) { + this.initComplete(); + return; + } + + temp_.extensionsLoadQueue = n; + + for (var i = 0; i < n; i++) { + id = cfg.extensions[i]; + if (id in pwlib.extensions) { + this.extensionLoaded(); + } else { + this.scriptLoad(base + id + '.js', this.extensionLoaded); + } + } + }; + + /** + * The load event handler for each extension script. + * @private + */ + this.extensionLoaded = function () { + temp_.extensionsLoadQueue--; + + if (temp_.extensionsLoadQueue === 0) { + var e = _self.config.extensions, + n = e.length; + + for (var i = 0; i < n; i++) { + if (!_self.extensionRegister(e[i])) { + _self.initError(pwlib.strf(lang.extensionRegisterFailed, {id: e[i]})); + return; + } + } + + _self.initComplete(); + } + }; + + /** + * Update the canvas scaling. This method determines the DPI and/or zoom level + * used by the browser to render the application. Based on these values, the + * canvas elements are scaled down to cancel any upscaling performed by the + * browser. + * + *

The {@link pwlib.appEvent.canvasSizeChange} application event is + * dispatched. + */ + this.updateCanvasScaling = function () { + var res = _self.resolution, + cs = win.getComputedStyle(res.elem, null), + image = _self.image; + bufferStyle = _self.buffer.canvas.style, + layerStyle = _self.layer.canvas.style, + scaleNew = 1, + width = parseInt(cs.width), + height = parseInt(cs.height); + + if (pwlib.browser.opera) { + // Opera zoom level detection. + // The scaling factor is sufficiently accurate for zoom levels between + // 100% and 200% (in steps of 10%). + + scaleNew = win.innerHeight / height; + scaleNew = MathRound(scaleNew * 10) / 10; + + } else if (width && !isNaN(width) && width !== res.dpiOptimal) { + // Page DPI detection. This only works in Gecko 1.9.1. + + res.dpiLocal = width; + + // The scaling factor is the same as in Gecko. + scaleNew = MathFloor(res.dpiLocal / res.dpiOptimal); + + } else if (pwlib.browser.olpcxo && pwlib.browser.gecko) { + // Support for the default Gecko included on the OLPC XO-1 system. + // + // See: + // http://www.robodesign.ro/mihai/blog/paintweb-performance + // http://mxr.mozilla.org/mozilla-central/source/gfx/src/thebes/nsThebesDeviceContext.cpp#725 + // dotsArePixels = false on the XO due to a hard-coded patch. + // Thanks go to roc from Mozilla for his feedback on making this work. + + res.dpiLocal = 134; // hard-coded value, we cannot determine it + + var appUnitsPerCSSPixel = 60, // hard-coded internally in Gecko + devPixelsPerCSSPixel = res.dpiLocal / res.dpiOptimal; // 1.3958333333 + appUnitsPerDevPixel = appUnitsPerCSSPixel / devPixelsPerCSSPixel; // 42.9850746278... + + scaleNew = appUnitsPerCSSPixel / MathFloor(appUnitsPerDevPixel); // 1.4285714285... + + // New in Gecko 1.9.2. + if ('mozImageSmoothingEnabled' in layerStyle) { + layerStyle.mozImageSmoothingEnabled + = bufferStyle.mozImageSmoothingEnabled = false; + } + } + + if (scaleNew === res.scale) { + return; + } + + res.scale = scaleNew; + + var styleWidth = image.width / res.scale * image.zoom, + styleHeight = image.height / res.scale * image.zoom; + + image.canvasScale = styleWidth / image.width; + + // FIXME: MSIE 9 clears the Canvas element when you change the + // elem.style.width/height... *argh* + bufferStyle.width = layerStyle.width = styleWidth + 'px'; + bufferStyle.height = layerStyle.height = styleHeight + 'px'; + + _self.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, + image.canvasScale)); + }; + + /** + * The Canvas events handler. + * + *

This method determines the mouse position relative to the canvas + * element, after which it invokes the method of the currently active tool + * with the same name as the current event type. For example, for the + * mousedown event the tool.mousedown() + * method is invoked. + * + *

The mouse coordinates are stored in the {@link PaintWeb#mouse} object. + * These properties take into account the current zoom level and the image + * scroll. + * + * @private + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the tool event handler executed, or false + * otherwise. + */ + this.ev_canvas = function (ev) { + if (!_self.tool) { + return false; + } + + switch (ev.type) { + case 'mousedown': + /* + * If the mouse is down already, skip the event. + * This is needed to allow the user to go out of the drawing canvas, + * release the mouse button, then come back and click to end the drawing + * operation. + * Additionally, this is needed to allow extensions like MouseKeys to + * perform their actions during a drawing operation, even when a real + * mouse is used. For example, allow the user to start drawing with the + * keyboard (press 0) then use the real mouse to move and click to end + * the drawing operation. + */ + if (_self.mouse.buttonDown) { + return false; + } + _self.mouse.buttonDown = true; + break; + + case 'mouseup': + // Skip the event if the mouse button was not down. + if (!_self.mouse.buttonDown) { + return false; + } + _self.mouse.buttonDown = false; + } + + /* + * Update the event, to include the mouse position, relative to the canvas + * element. + */ + if ('layerX' in ev) { + if (_self.image.canvasScale === 1) { + _self.mouse.x = ev.layerX; + _self.mouse.y = ev.layerY; + } else { + _self.mouse.x = MathRound(ev.layerX / _self.image.canvasScale); + _self.mouse.y = MathRound(ev.layerY / _self.image.canvasScale); + } + } else if ('offsetX' in ev) { + if (_self.image.canvasScale === 1) { + _self.mouse.x = ev.offsetX; + _self.mouse.y = ev.offsetY; + } else { + _self.mouse.x = MathRound(ev.offsetX / _self.image.canvasScale); + _self.mouse.y = MathRound(ev.offsetY / _self.image.canvasScale); + } + } + + // The event handler of the current tool. + if (ev.type in _self.tool && _self.tool[ev.type](ev)) { + ev.preventDefault(); + return true; + } else { + return false; + } + }; + + /** + * The global keyboard events handler. This makes all the keyboard shortcuts + * work in the web application. + * + *

This method determines the key the user pressed, based on the + * ev DOM Event object, taking into consideration any browser + * differences. Two new properties are added to the ev object: + * + *

    + *
  • ev.kid_ is a string holding the key and the modifiers list + * (Control, Alt and/or Shift). For + * example, if the user would press the key A while holding down + * Control, then ev.kid_ would be "Control A". If the + * user would press "9" while holding down Shift, then + * ev.kid_ would be "Shift 9". + * + *
  • ev.kobj_ holds a reference to the keyboard shortcut + * definition object from the configuration. This is useful for reuse, for + * passing parameters from the keyboard shortcut configuration object to the + * event handler. + *
+ * + *

In {@link PaintWeb.config.keys} one can setup the keyboard shortcuts. + * If the keyboard combination is found in that list, then the associated tool + * is activated. + * + *

Note: this method includes some work-around for making the image zoom + * keys work well both in Opera and Firefox. + * + * @private + * + * @param {Event} ev The DOM Event object. + * + * @see PaintWeb.config.keys The keyboard shortcuts configuration. + * @see pwlib.dom.KeyboardEventListener The class dealing with the + * cross-browser differences in the DOM keyboard events. + */ + this.ev_keyboard = function (ev) { + // Do not continue if the key was not recognized by the lib. + if (!ev.key_) { + return; + } + + if (ev.target && ev.target.nodeName) { + switch (ev.target.nodeName.toLowerCase()) { + case 'input': + if (ev.type === 'keypress' && (ev.key_ === 'Up' || ev.key_ === 'Down') + && ev.target.getAttribute('type') === 'number') { + _self.ev_numberInput(ev); + } + case 'select': + case 'textarea': + case 'button': return; + case 'div': if (ev.target.className==="inputText") { + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 : 변경된 내용이 없으면 skip + // FIXME: paindweb instance 객체 + //-------------------------------------------------------------------- + pw.image.modified = true; + return; + } + } + } + + // Rather ugly, but the only way, at the moment, to detect these keys in + // Opera and Firefox. + if (ev.type === 'keypress' && ev.char_) { + var isZoomKey = true, + imageZoomKeys = _self.config.imageZoomKeys; + + // Check if this is a zoom key and execute the commands as needed. + switch (ev.char_) { + case imageZoomKeys['in']: + _self.imageZoomIn(ev); + break; + + case imageZoomKeys['out']: + _self.imageZoomOut(ev); + break; + case imageZoomKeys['reset']: + _self.imageZoomReset(ev); + break; + default: + isZoomKey = false; + } + + if (isZoomKey) { + ev.preventDefault(); + return; + } + } + + // Determine the key ID. + ev.kid_ = ''; + var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'}; + for (i in kmods) { + if (ev[i] && ev.key_ !== kmods[i]) { + ev.kid_ += kmods[i] + ' '; + } + } + ev.kid_ += ev.key_; + + // Send the keyboard event to the event handler of the active tool. If it + // returns true, we consider it recognized the keyboard shortcut. + if (_self.tool && ev.type in _self.tool && _self.tool[ev.type](ev)) { + return true; + } + + // If there's no event handler within the active tool, or if the event + // handler does otherwise return false, then we continue with the global + // keyboard shortcuts. + + var gkey = _self.config.keys[ev.kid_]; + if (!gkey) { + return false; + } + + ev.kobj_ = gkey; + + // Check if the keyboard shortcut has some extension associated. + if ('extension' in gkey) { + var extension = _self.extensions[gkey.extension], + method = gkey.method || ev.type; + + // Call the extension method. + if (method in extension) { + extension[method].call(this, ev); + } + + } else if ('command' in gkey && gkey.command in _self.commands) { + // Invoke the command associated with the key. + _self.commands[gkey.command].call(this, ev); + + } else if (ev.type === 'keydown' && 'toolActivate' in gkey) { + + // Active the tool associated to the key. + _self.toolActivate(gkey.toolActivate, ev); + + } + + if (ev.type === 'keypress') { + ev.preventDefault(); + } + }; + + /** + * This is the keypress event handler for inputs of type=number. + * This function only handles cases when the key is Up or + * Down. For the Up key the input value is increased, + * and for the Down the value is decreased. + * + * @private + * @param {Event} ev The DOM Event object. + * @see PaintWeb#ev_keyboard + */ + this.ev_numberInput = function (ev) { + var target = ev.target; + + // Process the value. + var val, + max = parseFloat(target.getAttribute('max')), + min = parseFloat(target.getAttribute('min')), + step = parseFloat(target.getAttribute('step')); + + if (target.value === '' || target.value === null) { + val = !isNaN(min) ? min : 0; + } else { + val = parseFloat(target.value.replace(/[,.]+/g, '.'). + replace(/[^0-9.\-]/g, '')); + } + + // If target is not a number, then set the old value, or the minimum value. If all fails, set 0. + if (isNaN(val)) { + val = min || 0; + } + + if (isNaN(step)) { + step = 1; + } + + if (ev.shiftKey) { + step *= 2; + } + + if (ev.key_ === 'Down') { + step *= -1; + } + + val += step; + + if (!isNaN(max) && val > max) { + val = max; + } else if (!isNaN(min) && val < min) { + val = min; + } + + if (val == target.value) { + return; + } + + target.value = val; + + // Dispatch the 'change' events to make sure that any associated event + // handlers pick up the changes. + if (doc.createEvent && target.dispatchEvent) { + var ev_change = doc.createEvent('HTMLEvents'); + ev_change.initEvent('change', true, true); + target.dispatchEvent(ev_change); + } + }; + + /** + * Zoom into the image. + * + * @param {mixed} ev An event object which might have the shiftKey + * property. If the property evaluates to true, then the zoom level will + * increase twice more than normal. + * + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see PaintWeb#imageZoomTo The method used for changing the zoom level. + * @see PaintWeb.config.zoomStep The value used for increasing the zoom level. + */ + this.imageZoomIn = function (ev) { + if (ev && ev.shiftKey) { + _self.config.imageZoomStep *= 2; + } + + var res = _self.imageZoomTo('+'); + + if (ev && ev.shiftKey) { + _self.config.imageZoomStep /= 2; + } + + return res; + }; + + /** + * Zoom out of the image. + * + * @param {mixed} ev An event object which might have the shiftKey + * property. If the property evaluates to true, then the zoom level will + * decrease twice more than normal. + * + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see PaintWeb#imageZoomTo The method used for changing the zoom level. + * @see PaintWeb.config.zoomStep The value used for decreasing the zoom level. + */ + this.imageZoomOut = function (ev) { + if (ev && ev.shiftKey) { + _self.config.imageZoomStep *= 2; + } + + var res = _self.imageZoomTo('-'); + + if (ev && ev.shiftKey) { + _self.config.imageZoomStep /= 2; + } + + return res; + }; + + /** + * Reset the image zoom level to normal. + * + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see PaintWeb#imageZoomTo The method used for changing the zoom level. + */ + this.imageZoomReset = function (ev) { + return _self.imageZoomTo(1); + }; + + /** + * Change the image zoom level. + * + *

This method dispatches the {@link pwlib.appEvent.imageZoom} application + * event before zooming the image. Once the image zoom is applied, the {@link + * pwlib.appEvent.canvasSizeChange} event is dispatched. + * + * @param {Number|String} level The level you want to zoom the image to. + * + *

If the value is a number, it must be a floating point positive number, + * where 0.5 means 50%, 1 means 100% (normal) zoom, 4 means 400% and so on. + * + *

If the value is a string it must be "+" or "-". This means that the zoom + * level will increase/decrease using the configured {@link + * PaintWeb.config.zoomStep}. + * + * @returns {Boolean} True if the image zoom level changed successfully, or + * false if not. + */ + this.imageZoomTo = function (level) { + var image = this.image, + config = this.config, + res = this.resolution; + + if (!level) { + return false; + } else if (level === '+') { + level = Number(image.zoom) + config.imageZoomStep; + } else if (level === '-') { + level = Number(image.zoom) - config.imageZoomStep; + } else if (typeof level !== 'number') { + return false; + } + + if (level > config.imageZoomMax) { + level = config.imageZoomMax; + } else if (level < config.imageZoomMin) { + level = config.imageZoomMin; + } + + if (level === image.zoom) { + return true; + } + + var cancel = this.events.dispatch(new appEvent.imageZoom(level)); + if (cancel) { + return false; + } + + var styleWidth = image.width / res.scale * level, + styleHeight = image.height / res.scale * level, + bufferStyle = this.buffer.canvas.style, + layerStyle = this.layer.canvas.style; + + image.canvasScale = styleWidth / image.width; + + // FIXME: MSIE 9 clears the Canvas element when you change the + // elem.style.width/height... *argh* + bufferStyle.width = layerStyle.width = styleWidth + 'px'; + bufferStyle.height = layerStyle.height = styleHeight + 'px'; + + image.zoom = level; + + this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, image.canvasScale)); + + return true; + }; + + /** + * Crop the image. + * + *

The content of the image is retained only if the browser implements the + * getImageData and putImageData methods. + * + *

This method dispatches three application events: {@link + * pwlib.appEvent.imageSizeChange}, {@link pwlib.appEvent.canvasSizeChange} + * and {@link pwlib.appEvent.imageCrop}. The imageCrop event is + * dispatched before the image is cropped. The imageSizeChange + * and canvasSizeChange events are dispatched after the image is + * cropped. + * + * @param {Number} cropX Image cropping start position on the x-axis. + * @param {Number} cropY Image cropping start position on the y-axis. + * @param {Number} cropWidth Image crop width. + * @param {Number} cropHeight Image crop height. + * + * @returns {Boolean} True if the image was cropped successfully, or false if + * not. + */ + this.imageCrop = function (cropX, cropY, cropWidth, cropHeight) { + var bufferCanvas = this.buffer.canvas, + bufferContext = this.buffer.context, + image = this.image, + layerCanvas = this.layer.canvas, + layerContext = this.layer.context; + + cropX = parseInt(cropX); + cropY = parseInt(cropY); + cropWidth = parseInt(cropWidth); + cropHeight = parseInt(cropHeight); + + if (!cropWidth || !cropHeight || isNaN(cropX) || isNaN(cropY) || + isNaN(cropWidth) || isNaN(cropHeight) || cropX >= image.width || cropY + >= image.height) { + return false; + } + + var cancel = this.events.dispatch(new appEvent.imageCrop(cropX, cropY, + cropWidth, cropHeight)); + if (cancel) { + return false; + } + + if (cropWidth > this.config.imageWidthMax) { + cropWidth = this.config.imageWidthMax; + } + + if (cropHeight > this.config.imageHeightMax) { + cropHeight = this.config.imageHeightMax; + } + + if (cropX === 0 && cropY === 0 && image.width === cropWidth && image.height + === cropHeight) { + return true; + } + + var layerData = null, + bufferData = null, + layerState = this.stateSave(layerContext), + bufferState = this.stateSave(bufferContext), + scaledWidth = cropWidth * image.canvasScale, + scaledHeight = cropHeight * image.canvasScale, + dataWidth = MathMin(image.width, cropWidth), + dataHeight = MathMin(image.height, cropHeight), + sumX = cropX + dataWidth, + sumY = cropY + dataHeight; + + if (sumX > image.width) { + dataWidth -= sumX - image.width; + } + if (sumY > image.height) { + dataHeight -= sumY - image.height; + } + + if (layerContext.getImageData) { + // TODO: handle "out of memory" errors. + try { + layerData = layerContext.getImageData(cropX, cropY, dataWidth, + dataHeight); + } catch (err) { } + } + + if (bufferContext.getImageData) { + try { + bufferData = bufferContext.getImageData(cropX, cropY, dataWidth, + dataHeight); + } catch (err) { } + } + + bufferCanvas.style.width = layerCanvas.style.width = scaledWidth + 'px'; + bufferCanvas.style.height = layerCanvas.style.height = scaledHeight + 'px'; + + layerCanvas.width = cropWidth; + layerCanvas.height = cropHeight; + + if (layerData && layerContext.putImageData) { + layerContext.putImageData(layerData, 0, 0); + } + + this.stateRestore(layerContext, layerState); + state = this.stateSave(bufferContext); + + bufferCanvas.width = cropWidth; + bufferCanvas.height = cropHeight; + + if (bufferData && bufferContext.putImageData) { + bufferContext.putImageData(bufferData, 0, 0); + } + + this.stateRestore(bufferContext, bufferState); + + image.width = cropWidth; + image.height = cropHeight; + + bufferState = layerState = layerData = bufferData = null; + + this.events.dispatch(new appEvent.imageSizeChange(cropWidth, cropHeight)); + this.events.dispatch(new appEvent.canvasSizeChange(scaledWidth, + scaledHeight, image.canvasScale)); + + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 : 변경된 내용이 없으면 skip + // FIXME: paindweb instance 객체 + //-------------------------------------------------------------------- + this.image.modified = true; + + return true; + }; + + /** + * Save the state of a Canvas context. + * + * @param {CanvasRenderingContext2D} context The 2D context of the Canvas + * element you want to save the state. + * + * @returns {Object} The object has all the state properties and values. + */ + this.stateSave = function (context) { + if (!context || !context.canvas || !this.stateProperties) { + return false; + } + + var stateObj = {}, + prop = null, + n = this.stateProperties.length; + + for (var i = 0; i < n; i++) { + prop = this.stateProperties[i]; + stateObj[prop] = context[prop]; + } + + return stateObj; + }; + + /** + * Restore the state of a Canvas context. + * + * @param {CanvasRenderingContext2D} context The 2D context where you want to + * restore the state. + * + * @param {Object} stateObj The state object saved by the {@link + * PaintWeb#stateSave} method. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.stateRestore = function (context, stateObj) { + if (!context || !context.canvas) { + return false; + } + + for (var state in stateObj) { + context[state] = stateObj[state]; + } + + return true; + }; + + /** + * Allow shadows. This method re-enabled shadow rendering, if it was enabled + * before shadows were disallowed. + * + *

The {@link pwlib.appEvent.shadowAllow} event is dispatched. + */ + this.shadowAllow = function () { + if (this.shadowAllowed || !this.shadowSupported) { + return; + } + + // Note that some daily builds of Webkit in Chromium fail to render the + // shadow when context.drawImage() is used (see the this.layerUpdate()). + var context = this.layer.context, + cfg = this.config.shadow; + + if (cfg.enable) { + context.shadowColor = cfg.shadowColor; + context.shadowOffsetX = cfg.shadowOffsetX; + context.shadowOffsetY = cfg.shadowOffsetY; + context.shadowBlur = cfg.shadowBlur; + } + + this.shadowAllowed = true; + + this.events.dispatch(new appEvent.shadowAllow(true)); + }; + + /** + * Disallow shadows. This method disables shadow rendering, if it is enabled. + * + *

The {@link pwlib.appEvent.shadowAllow} event is dispatched. + */ + this.shadowDisallow = function () { + if (!this.shadowAllowed || !this.shadowSupported) { + return; + } + + if (this.config.shadow.enable) { + var context = this.layer.context; + context.shadowColor = 'rgba(0,0,0,0)'; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 0; + } + + this.shadowAllowed = false; + + this.events.dispatch(new appEvent.shadowAllow(false)); + }; + + /** + * Update the current image layer by moving the pixels from the buffer onto + * the layer. This method also adds a point into the history. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.layerUpdate = function () { + this.layer.context.drawImage(this.buffer.canvas, 0, 0); + this.buffer.context.clearRect(0, 0, this.image.width, this.image.height); + this.historyAdd(); + + return true; + }; + + /** + * Add the current image layer to the history. + * + *

Once the history state has been updated, this method dispatches the + * {@link pwlib.appEvent.historyUpdate} event. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + // TODO: some day it would be nice to implement a hybrid history system. + this.historyAdd = function () { + var layerContext = this.layer.context, + history = this.history, + prevPos = history.pos; + + if (!layerContext.getImageData) { + return false; + } + + // We are in an undo-step, trim until the end, eliminating any possible redo-steps. + if (prevPos < history.states.length) { + history.states.splice(prevPos, history.states.length); + } + + // TODO: in case of "out of memory" errors... I should show up some error. + try { + history.states.push(layerContext.getImageData(0, 0, this.image.width, + this.image.height)); + } catch (err) { + return false; + } + + // If we have too many history ImageDatas, remove the oldest ones + if ('historyLimit' in this.config && + history.states.length > this.config.historyLimit) { + + history.states.splice(0, history.states.length + - this.config.historyLimit); + } + history.pos = history.states.length; + + this.image.modified = true; + + this.events.dispatch(new appEvent.historyUpdate(history.pos, prevPos, + history.pos)); + + return true; + }; + + /** + * Jump to any ImageData/position in the history. + * + *

Once the history state has been updated, this method dispatches the + * {@link pwlib.appEvent.historyUpdate} event. + * + * @param {Number|String} pos The history position to jump to. + * + *

If the value is a number, then it must point to an existing index in the + * {@link PaintWeb#history}.states array. + * + *

If the value is a string, it must be "undo" or "redo". + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.historyGoto = function (pos) { + var layerContext = this.layer.context, + image = this.image, + history = this.history; + + if (!history.states.length || !layerContext.putImageData) { + return false; + } + + var cpos = history.pos; + + if (pos === 'undo') { + pos = cpos-1; + } else if (pos === 'redo') { + pos = cpos+1; + } + + if (pos < 1 || pos > history.states.length) { + return false; + } + + var himg = history.states[pos-1]; + if (!himg) { + return false; + } + + // Each image in the history can have a different size. As such, the script + // must take this into consideration. + var w = MathMin(image.width, himg.width), + h = MathMin(image.height, himg.height); + + layerContext.clearRect(0, 0, image.width, image.height); + + try { + // Firefox 3 does not clip the image, if needed. + layerContext.putImageData(himg, 0, 0, 0, 0, w, h); + + } catch (err) { + // The workaround is to use a new canvas from which we can copy the + // history image without causing any exceptions. + var tmp = doc.createElement('canvas'); + tmp.width = himg.width; + tmp.height = himg.height; + + var tmp2 = tmp.getContext('2d'); + tmp2.putImageData(himg, 0, 0); + + layerContext.drawImage(tmp, 0, 0); + + tmp2 = tmp = null; + delete tmp2, tmp; + } + + history.pos = pos; + + this.events.dispatch(new appEvent.historyUpdate(pos, cpos, + history.states.length)); + + return true; + }; + + /** + * Clear the image history. + * + *

This method dispatches the {@link pwlib.appEvent.historyUpdate} event. + * + * @private + */ + this.historyReset = function () { + this.history.pos = 0; + this.history.states = []; + + this.events.dispatch(new appEvent.historyUpdate(0, 0, 0)); + }; + + /** + * Perform horizontal/vertical line snapping. This method updates the mouse + * coordinates to "snap" with the given coordinates. + * + * @param {Number} x The x-axis location. + * @param {Number} y The y-axis location. + */ + this.toolSnapXY = function (x, y) { + var diffx = MathAbs(_self.mouse.x - x), + diffy = MathAbs(_self.mouse.y - y); + + if (diffx > diffy) { + _self.mouse.y = y; + } else { + _self.mouse.x = x; + } + }; + + /** + * Activate a drawing tool by ID. + * + *

The id provided must be of an existing drawing tool, one that + * has been installed. + * + *

The ev argument is an optional DOM Event object which is + * useful when dealing with different types of tool activation, either by + * keyboard or by mouse events. Tool-specific code can implement different + * functionality based on events. + * + *

This method dispatches the {@link pwlib.appEvent.toolPreactivate} event + * before creating the new tool instance. Once the new tool is successfully + * activated, the {@link pwlib.appEvent.toolActivate} event is also + * dispatched. + * + * @param {String} id The ID of the drawing tool to be activated. + * @param {Event} [ev] The DOM Event object. + * + * @returns {Boolean} True if the tool has been activated, or false if not. + * + * @see PaintWeb#toolRegister Register a new drawing tool. + * @see PaintWeb#toolUnregister Unregister a drawing tool. + * + * @see pwlib.tools The object holding all the drawing tools. + * @see pwlib.appEvent.toolPreactivate + * @see pwlib.appEvent.toolActivate + */ + this.toolActivate = function (id, ev) { + if (!id || !(id in pwlib.tools) || typeof pwlib.tools[id] !== 'function') { + return false; + } + + var tool = pwlib.tools[id], + prevId = this.tool ? this.tool._id : null; + + if (prevId && this.tool instanceof pwlib.tools[id]) { + return true; + } + + var cancel = this.events.dispatch(new appEvent.toolPreactivate(id, prevId)); + if (cancel) { + return false; + } + + var tool_obj = new tool(this, ev); + if (!tool_obj) { + return false; + } + + /* + * Each tool can implement its own mouse and keyboard events handler. + * Additionally, tool objects can implement handlers for the deactivation + * and activation events. + * Given tool1 is active and tool2 is going to be activated, then the + * following event handlers will be called: + * + * tool2.preActivate + * tool1.deactivate + * tool2.activate + * + * In the "preActivate" event handler you can cancel the tool activation by + * returning a value which evaluates to false. + */ + + if ('preActivate' in tool_obj && !tool_obj.preActivate(ev)) { + tool_obj = null; + return false; + } + + // Deactivate the previously active tool + if (this.tool && 'deactivate' in this.tool) { + this.tool.deactivate(ev); + } + + this.tool = tool_obj; + + this.mouse.buttonDown = false; + + // Besides the "constructor", each tool can also have code which is run + // after the deactivation of the previous tool. + if ('activate' in this.tool) { + this.tool.activate(ev); + } + + this.events.dispatch(new appEvent.toolActivate(id, prevId)); + + return true; + }; + + /** + * Register a new drawing tool into PaintWeb. + * + *

This method dispatches the {@link pwlib.appEvent.toolRegister} + * application event. + * + * @param {String} id The ID of the new tool. The tool object must exist in + * {@link pwlib.tools}. + * + * @returns {Boolean} True if the tool was successfully registered, or false + * if not. + * + * @see PaintWeb#toolUnregister allows you to unregister tools. + * @see pwlib.tools Holds all the drawing tools. + * @see pwlib.appEvent.toolRegister + */ + this.toolRegister = function (id) { + if (typeof id !== 'string' || !id) { + return false; + } + + // TODO: it would be very nice to create the tool instance on register, for + // further extensibility. + + var tool = pwlib.tools[id]; + if (typeof tool !== 'function') { + return false; + } + + tool.prototype._id = id; + + this.events.dispatch(new appEvent.toolRegister(id)); + + if (!this.tool && id === this.config.toolDefault) { + return this.toolActivate(id); + } else { + return true; + } + }; + + /** + * Unregister a drawing tool from PaintWeb. + * + *

This method dispatches the {@link pwlib.appEvent.toolUnregister} + * application event. + * + * @param {String} id The ID of the tool you want to unregister. + * + * @returns {Boolean} True if the tool was unregistered, or false if it does + * not exist or some error occurred. + * + * @see PaintWeb#toolRegister allows you to register new drawing tools. + * @see pwlib.tools Holds all the drawing tools. + * @see pwlib.appEvent.toolUnregister + */ + this.toolUnregister = function (id) { + if (typeof id !== 'string' || !id || !(id in pwlib.tools)) { + return false; + } + + this.events.dispatch(new appEvent.toolUnregister(id)); + + return true; + }; + + /** + * Register a new extension into PaintWeb. + * + *

If the extension object being constructed has the + * extensionRegister() method, then it will be invoked, allowing + * any custom extension registration code to run. If the method returns false, + * then the extension will not be registered. + * + *

Once the extension is successfully registered, this method dispatches + * the {@link pwlib.appEvent.extensionRegister} application event. + * + * @param {String} id The ID of the new extension. The extension object + * constructor must exist in {@link pwlib.extensions}. + * + * @returns {Boolean} True if the extension was successfully registered, or + * false if not. + * + * @see PaintWeb#extensionUnregister allows you to unregister extensions. + * @see PaintWeb#extensions Holds all the instances of registered extensions. + * @see pwlib.extensions Holds all the extension classes. + */ + this.extensionRegister = function (id) { + if (typeof id !== 'string' || !id) { + return false; + } + + var func = pwlib.extensions[id]; + if (typeof func !== 'function') { + return false; + } + + func.prototype._id = id; + + var obj = new func(_self); + + if ('extensionRegister' in obj && !obj.extensionRegister()) { + return false; + } + + this.extensions[id] = obj; + this.events.dispatch(new appEvent.extensionRegister(id)); + + return true; + }; + + /** + * Unregister an extension from PaintWeb. + * + *

If the extension object being destructed has the + * extensionUnregister() method, then it will be invoked, + * allowing any custom extension removal code to run. + * + *

Before the extension is unregistered, this method dispatches the {@link + * pwlib.appEvent.extensionUnregister} application event. + * + * @param {String} id The ID of the extension object you want to unregister. + * + * @returns {Boolean} True if the extension was removed, or false if it does + * not exist or some error occurred. + * + * @see PaintWeb#extensionRegister allows you to register new extensions. + * @see PaintWeb#extensions Holds all the instances of registered extensions. + * @see pwlib.extensions Holds all the extension classes. + */ + this.extensionUnregister = function (id) { + if (typeof id !== 'string' || !id || !(id in this.extensions)) { + return false; + } + + this.events.dispatch(new appEvent.extensionUnregister(id)); + + if ('extensionUnregister' in this.extensions[id]) { + this.extensions[id].extensionUnregister(); + } + delete this.extensions[id]; + + return true; + }; + + /** + * Register a new command in PaintWeb. Commands are simple function objects + * which can be invoked by keyboard shortcuts or by GUI elements. + * + *

Once the command is successfully registered, this method dispatches the + * {@link pwlib.appEvent.commandRegister} application event. + * + * @param {String} id The ID of the new command. + * @param {Function} func The command function. + * + * @returns {Boolean} True if the command was successfully registered, or + * false if not. + * + * @see PaintWeb#commandUnregister allows you to unregister commands. + * @see PaintWeb#commands Holds all the registered commands. + */ + this.commandRegister = function (id, func) { + if (typeof id !== 'string' || !id || typeof func !== 'function' || id in + this.commands) { + return false; + } + + this.commands[id] = func; + this.events.dispatch(new appEvent.commandRegister(id)); + + return true; + }; + + /** + * Unregister a command from PaintWeb. + * + *

Before the command is unregistered, this method dispatches the {@link + * pwlib.appEvent.commandUnregister} application event. + * + * @param {String} id The ID of the command you want to unregister. + * + * @returns {Boolean} True if the command was removed successfully, or false + * if not. + * + * @see PaintWeb#commandRegister allows you to register new commands. + * @see PaintWeb#commands Holds all the registered commands. + */ + this.commandUnregister = function (id) { + if (typeof id !== 'string' || !id || !(id in this.commands)) { + return false; + } + + this.events.dispatch(new appEvent.commandUnregister(id)); + + delete this.commands[id]; + + return true; + }; + + /** + * Load a script into the document. + * + * @param {String} url The script URL you want to insert. + * @param {Function} [handler] The load event handler you want. + */ + this.scriptLoad = function (url, handler) { + if (!handler) { + var elem = doc.createElement('script'); + elem.type = 'text/javascript'; + elem.src = url; + this.elems.head.appendChild(elem); + return; + } + + // huh, use XHR then eval() the code. + // browsers do not dispatch the 'load' event reliably for script elements. + + /** @ignore */ + var xhr = new XMLHttpRequest(); + + /** @ignore */ + xhr.onreadystatechange = function () { + if (!xhr || xhr.readyState !== 4) { + return; + + } else if ((xhr.status !== 304 && xhr.status !== 200) || + !xhr.responseText) { + handler(false, xhr); + + } else { + try { + eval.call(win, xhr.responseText); + } catch (err) { + eval(xhr.responseText, win); + } + handler(true, xhr); + } + + xhr = null; + }; + + xhr.open('GET', url); + xhr.send(''); + }; + + /** + * Insert a stylesheet into the document. + * + * @param {String} id The stylesheet ID. This is used to avoid inserting the + * same style in the document. + * @param {String} url The URL of the stylesheet you want to insert. + * @param {String} [media='screen, projection'] The media attribute. + * @param {Function} [handler] The load event handler. + */ + this.styleLoad = function (id, url, media, handler) { + id = 'paintweb_style_' + id; + + var elem = doc.getElementById(id); + if (elem) { + return; + } + + if (!media) { + media = 'screen, projection'; + } + + elem = doc.createElement('link'); + + if (handler) { + elem.addEventListener('load', handler, false); + } + + elem.id = id; + elem.rel = 'stylesheet'; + elem.type = 'text/css'; + elem.media = media; + elem.href = url; + + this.elems.head.appendChild(elem); + }; + + /** + * Perform action undo. + * + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see PaintWeb#historyGoto The method invoked by this command. + */ + this.historyUndo = function () { + return _self.historyGoto('undo'); + }; + + /** + * Perform action redo. + * + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see PaintWeb#historyGoto The method invoked by this command. + */ + this.historyRedo = function () { + return _self.historyGoto('redo'); + }; + + /** + * Load an image. By loading an image the history is cleared and the Canvas + * dimensions are updated to fit the new image. + * + *

This method dispatches two application events: {@link + * pwlib.appEvent.imageSizeChange} and {@link + * pwlib.appEvent.canvasSizeChange}. + * + * @param {Element} importImage The image element you want to load into the + * Canvas. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.imageLoad = function (importImage) { + if (!importImage || !importImage.width || !importImage.height || + importImage.nodeType !== this.ELEMENT_NODE || + !pwlib.isSameHost(importImage.src, win.location.host)) { + return false; + } + + this.historyReset(); + + var layerContext = this.layer.context, + layerCanvas = this.layer.canvas, + layerStyle = layerCanvas.style, + bufferCanvas = this.buffer.canvas, + bufferStyle = bufferCanvas.style, + image = this.image, + styleWidth = importImage.width * image.canvasScale, + styleHeight = importImage.height * image.canvasScale, + result = true; + + bufferCanvas.width = layerCanvas.width = importImage.width; + bufferCanvas.height = layerCanvas.height = importImage.height; + bufferStyle.width = layerStyle.width = styleWidth + 'px'; + bufferStyle.height = layerStyle.height = styleHeight + 'px'; + + try { + layerContext.drawImage(importImage, 0, 0); + } catch (err) { + result = false; + bufferCanvas.width = layerCanvas.width = image.width; + bufferCanvas.height = layerCanvas.height = image.height; + styleWidth = image.width * image.canvasScale; + styleHeight = image.height * image.canvasScale; + bufferStyle.width = layerStyle.width = styleWidth + 'px'; + bufferStyle.height = layerStyle.height = styleHeight + 'px'; + } + + if (result) { + image.width = importImage.width; + image.height = importImage.height; + _self.config.imageLoad = importImage; + + this.events.dispatch(new appEvent.imageSizeChange(image.width, + image.height)); + + this.events.dispatch(new appEvent.canvasSizeChange(styleWidth, styleHeight, + image.canvasScale)); + } + + this.historyAdd(); + image.modified = false; + + return result; + }; + + /** + * Clear the image. + */ + this.imageClear = function (ev) { + var layerContext = _self.layer.context, + image = _self.image; + + layerContext.clearRect(0, 0, image.width, image.height); + + // Set the configured background color. + var fillStyle = layerContext.fillStyle; + layerContext.fillStyle = _self.config.backgroundColor; + layerContext.fillRect(0, 0, image.width, image.height); + layerContext.fillStyle = fillStyle; + + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 - 삭제를 구분하기 위해 이미지 size를 0으로 reset + //-------------------------------------------------------------------- + image.height = 0; + image.width = 0; + pw.image.modified = true; + //-------------------------------------------------------------------- + + _self.historyAdd(); + }; + + /** + * Save the image. + * + *

This method dispatches the {@link pwlib.appEvent.imageSave} event. + * + *

Note: the "Save image" operation relies on integration + * extensions. A vanilla configuration of PaintWeb will simply open the the + * image in a new tab using a data: URL. You must have some event listener for + * the imageSave event and you must prevent the default action. + * + *

If the default action for the imageSave application event + * is not prevented, then this method will also dispatch the {@link + * pwlib.appEvent.imageSaveResult} application event. + * + *

Your event handler for the imageSave event must dispatch + * the imageSaveResult event. + * + * @param {String} [type="auto"] Image MIME type. This tells the browser which + * format to use when saving the image. If the image format type is not + * supported, then the image is saved as PNG. + * + *

You can use the resulting data URL to check which is the actual image + * format. + * + *

When type is "auto" then PaintWeb checks the type of the + * image currently loaded ({@link PaintWeb.config.imageLoad}). If the format + * is recognized, then the same format is used to save the image. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.imageSave = async function (type) { + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 : 변경된 내용이 없으면 skip + // FIXME: paindweb instance 객체 + //-------------------------------------------------------------------- + if(!pw.image.modified){ + if(pw.history.pos === 1) { + alert('변경된 내용이 없습니다.'); + return false; + } + } + //-------------------------------------------------------------------- + + + var canvas = _self.layer.canvas, + cfg = _self.config, + img = _self.image, + imageLoad = _self.config.imageLoad, + ext = 'png', idata = null, src = null, pos; + + if (!canvas.toDataURL) { + return false; + } + + var extMap = { + 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif' + }; + + // Detect the MIME type of the image currently loaded. + if (typeof type !== 'string' || !type) { + if (imageLoad && imageLoad.src && imageLoad.src.substr(0, 5) !== 'data:') { + src = imageLoad.src; + pos = src.indexOf('?'); + if (pos !== -1) { + src = src.substr(0, pos); + } + ext = src.substr(src.lastIndexOf('.') + 1).toLowerCase(); + } + + type = extMap[ext] || 'image/png'; + } + + // We consider that other formats than PNG do not support transparencies. + // Thus, we create a new Canvas element for which we set the configured + // background color, and we render the image onto it. + if (type !== 'image/png' || img.zoom != 1) { + canvas = doc.createElement('canvas'); + var context = canvas.getContext('2d'); + + canvas.width = img.width * img.zoom; + canvas.height = img.height * img.zoom; + + context.fillStyle = cfg.backgroundColor; + context.fillRect(0, 0, img.width, img.height); + context.drawImage(_self.layer.canvas, 0, 0, canvas.width, canvas.height); + + context = null; + } + + try { + // canvas.toDataURL('image/jpeg', quality) fails in Gecko due to security + // concerns, uh-oh. + if (type === 'image/jpeg' && !pwlib.browser.gecko) { + idata = canvas.toDataURL(type, cfg.jpegSaveQuality); + } else { + idata = canvas.toDataURL(type); + } + } catch (err) { + alert(lang.errorImageSave + "\n" + err); + return false; + } + + canvas = null; +// alert('idata~~~'+idata) + if (!idata || idata === 'data:,') { + return false; + } + + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 : save image + //-------------------------------------------------------------------- + if (!_self.config.imageSave) { + if (_self.config.afterImageSave) _self.config.afterImageSave(); + return; + } + + let fileType = 'image/'; + const regx = new RegExp('.(gif|jpg|jpeg|tiff|png|ico)$', 'i') + let name = (/[^(/|\\)]*$/).exec(idata)[0] + // 20220915_5df30439017240afbc7081ae9bfbfa2f_e 와 같은 형태인 경우 imageName으로 + // 원본파일명 + '_e' + if(!regx.exec(name)){ + const imgName = srcImg.name; + const idx = imgName.indexOf('.'); + + // 삭제가 아닌 경우 원본이미지명 + '_e' 추가 + if(img.width !== 0){ + name = imgName.substring(0, idx)+'_e'+ imgName.substring(idx); + } + } + fileType += regx.test(name) ? regx.exec(name)[0].replace('.', '') : 'jpg' + // mine type 'jpg' > jpeg로 변경 + fileType = fileType.replace('image/jpg', 'image/jpeg'); + const bstr = atob(idata.split(",")[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while(n--) { + u8arr[n] = bstr.charCodeAt(n); + } + const file = new File([u8arr], name, {type:fileType}); + + // 이미지 삭제인 경우 img.width, img.height 가 0 + _self.config.imageSave(file, img.width, img.height, doc, _self); + + + return; + //--------------------------------------------------------------------- + + + //-------------------------------------------------------------------- + // FIXME: 변경된 부분 : 이후 아래 부분 skip - 원본 로직 + //-------------------------------------------------------------------- + + var ev = new appEvent.imageSave(idata, img.width, img.height), + cancel = _self.events.dispatch(ev); + + if (cancel) { + return true; + } + + idata = null; + + _self.events.dispatch(new appEvent.imageSaveResult(true)); + + return true; + }; + + /** + * image rotation. add by gujc. + */ + + this.imageRotate = function (type) { + var layerCanvas = _self.layer.canvas, + layerCtx = _self.layer.context, + bufferCanvas = _self.buffer.canvas, + bufferCtx = _self.buffer.context, + img = _self.image; + var bufferStyle = _self.buffer.canvas.style, + layerStyle = layerCanvas.style; + + + var srcCanvas = doc.createElement('canvas'); + var srcCtx=srcCanvas.getContext("2d"); + + srcCanvas.height = layerCanvas.width; + srcCanvas.width = layerCanvas.height; + srcCtx.translate(layerCanvas.height/2,layerCanvas.width/2); + srcCtx.rotate(90*Math.PI/180); + srcCtx.drawImage(layerCanvas,-layerCanvas.width/2,-layerCanvas.height/2); + + var lineWidth = bufferCtx.lineWidth; + var strokeStyle = bufferCtx.strokeStyle; + var fillStyle = bufferCtx.fillStyle; + layerCanvas.width = bufferCanvas.width = srcCanvas.width; + layerCanvas.height = bufferCanvas.height = srcCanvas.height; + bufferCtx.lineWidth = layerCtx.lineWidth = lineWidth; + bufferCtx.strokeStyle = layerCtx.strokeStyle = strokeStyle; + bufferCtx.fillStyle = layerCtx.fillStyle = fillStyle; + + layerCtx.drawImage(srcCanvas, 0, 0); + + img.width = layerCanvas.width; + img.height = layerCanvas.height; + _self.events.dispatch(new appEvent.imageSizeChange(layerCanvas.width, layerCanvas.height)); + + bufferStyle.width = layerStyle.width = layerCanvas.width + 'px'; + bufferStyle.height = layerStyle.height = layerCanvas.height + 'px'; + + _self.events.dispatch(new appEvent.canvasSizeChange(layerCanvas.width, layerCanvas.height, img.canvasScale)); + + var zoom = _self.image.zoom; + _self.image.zoom=1; + _self.imageZoomTo(zoom); + + return true; + }; + + /** + * 밝기 조절(이미지 밝게) + */ + this.imageBright = function (type) { + _self.brightnessSliderChange(20); + }; + + /** + * 밝기 조절(이미지 어둡게) + */ + this.imageDark = function (type) { + _self.brightnessSliderChange(-20); + }; + + this.brightnessSliderChange = function(step){ + + _self.brightnessSliderValue += step; + if(_self.brightnessSliderValue < -255){ + _self.brightnessSliderValue = -255; + } else if(_self.brightnessSliderValue > 255){ + _self.brightnessSliderValue = 255; + } + + // imageData를 가져온다. + let orginlCanvasWidth = _self.orginl.canvas.width; + let orginlCanvasHeight = _self.orginl.canvas.height; + let orginlCtx = _self.orginl.context; + + let pixels = orginlCtx.getImageData(0,0, orginlCanvasWidth, orginlCanvasHeight); + + // image processing + let filteredData = _self.brightnessCalc(pixels, _self.brightnessSliderValue); + + // Canvas에 다시 그린다. + let jobCtx = _self.layer.context; + jobCtx.putImageData(filteredData, 0 , 0); + + } + + this.brightnessCalc = function(pixels, sliderValue) { + let imgSource = pixels.data; + + for(let i =0; i< imgSource.length; i+=4){ + let tempR = imgSource[i]; + let tempG = imgSource[i+1]; + let tempB = imgSource[i+2]; + + tempR += sliderValue; + tempG += sliderValue; + tempB += sliderValue; + + if(tempR < 0){ + tempR = 0; + } else if(tempR > 255){ + tempR = 255; + } + + if(tempG < 0){ + tempG = 0; + } else if(tempG > 255){ + tempG = 255; + } + + if(tempB < 0){ + tempB = 0; + } else if(tempB > 255){ + tempB = 255; + } + + imgSource[i] = tempR; + imgSource[i+1] = tempG; + imgSource[i+2] = tempB; + } + return pixels; + } + + /** + * The imageSaveResult application event handler. This method + * PaintWeb-related stuff: for example, the {@link PaintWeb.image.modified} + * flag is turned to false. + * + * @private + * + * @param {pwlib.appEvent.imageSaveResult} ev The application event object. + * + * @see {PaintWeb#imageSave} The method which allows you to save the image. + */ + this.imageSaveResultHandler = function (ev) { + if (ev.successful) { + _self.image.modified = false; + } + }; + + /** + * Swap the fill and stroke styles. This is just like in Photoshop, if the + * user presses X, the fill/stroke colors are swapped. + * + *

This method dispatches the {@link pwlib.appEvent.configChange} event + * twice for each color (strokeStyle and fillStyle). + */ + this.swapFillStroke = function () { + var fillStyle = _self.config.fillStyle, + strokeStyle = _self.config.strokeStyle; + + _self.config.fillStyle = strokeStyle; + _self.config.strokeStyle = fillStyle; + + var ev = new appEvent.configChange(strokeStyle, fillStyle, 'fillStyle', '', + _self.config); + + _self.events.dispatch(ev); + + ev = new appEvent.configChange(fillStyle, strokeStyle, 'strokeStyle', '', + _self.config); + + _self.events.dispatch(ev); + }; + + /** + * Select all the pixels. This activates the selection tool, and selects the + * entire image. + * + * @param {Event} [ev] The DOM Event object which generated the request. + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see {pwlib.tools.selection.selectAll} The command implementation. + */ + this.selectAll = function (ev) { + if (_self.toolActivate('selection', ev)) { + return _self.tool.selectAll(ev); + } else { + return false; + } + }; + + /** + * Cut the available selection. This only works when the selection tool is + * active and when some selection is available. + * + * @param {Event} [ev] The DOM Event object which generated the request. + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see {pwlib.tools.selection.selectionCut} The command implementation. + */ + this.selectionCut = function (ev) { + if (!_self.tool || _self.tool._id !== 'selection') { + return false; + } else { + return _self.tool.selectionCut(ev); + } + }; + + /** + * Copy the available selection. This only works when the selection tool is + * active and when some selection is available. + * + * @param {Event} [ev] The DOM Event object which generated the request. + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see {pwlib.tools.selection.selectionCopy} The command implementation. + */ + this.selectionCopy = function (ev) { + if (!_self.tool || _self.tool._id !== 'selection') { + return false; + } else { + return _self.tool.selectionCopy(ev); + } + }; + + this.selectionCrop = function (ev) { + if (!_self.tool || _self.tool._id !== 'selection') { + return false; + } else { + return _self.tool.selectionCrop(ev); + } + }; + + this.selectionFill = function (ev) { + if (!_self.tool || _self.tool._id !== 'selection') { + return false; + } else { + return _self.tool.selectionFill(ev); + } + }; + + /** + * Paste the current clipboard image. This only works when some ImageData is + * available in {@link PaintWeb#clipboard}. + * + * @param {Event} [ev] The DOM Event object which generated the request. + * @returns {Boolean} True if the operation was successful, or false if not. + * + * @see {pwlib.tools.selection.clipboardPaste} The command implementation. + */ + this.clipboardPaste = function (ev) { + if (!_self.clipboard || !_self.toolActivate('selection', ev)) { + return false; + } else { + return _self.tool.clipboardPaste(ev); + } + }; + + /** + * The configChange application event handler. This method + * updates the Canvas context properties depending on which configuration + * property changed. + * + * @private + * @param {pwlib.appEvent.configChange} ev The application event object. + */ + this.configChangeHandler = function (ev) { + if (ev.group === 'shadow' && _self.shadowSupported && _self.shadowAllowed) { + var context = _self.layer.context, + cfg = ev.groupRef; + + // Enable/disable shadows + if (ev.config === 'enable') { + if (ev.value) { + context.shadowColor = cfg.shadowColor; + context.shadowOffsetX = cfg.shadowOffsetX; + context.shadowOffsetY = cfg.shadowOffsetY; + context.shadowBlur = cfg.shadowBlur; + } else { + context.shadowColor = 'rgba(0,0,0,0)'; + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 0; + } + return; + } + + // Do not update any context properties if shadows are not enabled. + if (!cfg.enable) { + return; + } + + switch (ev.config) { + case 'shadowBlur': + case 'shadowOffsetX': + case 'shadowOffsetY': + ev.value = parseInt(ev.value); + case 'shadowColor': + context[ev.config] = ev.value; + } + + } else if (ev.group === 'line') { + switch (ev.config) { + case 'lineWidth': + case 'miterLimit': + ev.value = parseInt(ev.value); + case 'lineJoin': + case 'lineCap': + _self.buffer.context[ev.config] = ev.value; + } + + } else if (ev.group === 'text') { + switch (ev.config) { + case 'textAlign': + case 'textBaseline': + _self.buffer.context[ev.config] = ev.value; + } + + } else if (!ev.group) { + switch (ev.config) { + case 'fillStyle': + case 'strokeStyle': + _self.buffer.context[ev.config] = ev.value; + } + } + }; + + /** + * Destroy a PaintWeb instance. This method allows you to unload a PaintWeb + * instance. Extensions, tools and commands are unregistered, and the GUI + * elements are removed. + * + *

The scripts and styles loaded are not removed, since they might be used + * by other PaintWeb instances. + * + *

The {@link pwlib.appEvent.appDestroy} application event is dispatched + * before the current instance is destroyed. + */ + this.destroy = function () { + this.events.dispatch(new appEvent.appDestroy()); + + for (var cmd in this.commands) { + this.commandUnregister(cmd); + } + + for (var ext in this.extensions) { + this.extensionUnregister(ext); + } + + for (var tool in this.gui.tools) { + this.toolUnregister(tool); + } + + this.gui.destroy(); + + this.initialized = PaintWeb.INIT_NOT_STARTED; + }; + + this.toString = function () { + return 'PaintWeb v' + this.version + ' (build ' + this.build + ')'; + }; + + + preInit(); +}; + +/** + * Application initialization not started. + * @constant + */ +PaintWeb.INIT_NOT_STARTED = 0; + +/** + * Application initialization started. + * @constant + */ +PaintWeb.INIT_STARTED = 1; + +/** + * Application initialization completed successfully. + * @constant + */ +PaintWeb.INIT_DONE = 2; + +/** + * Application initialization failed. + * @constant + */ +PaintWeb.INIT_ERROR = -1; + +/** + * PaintWeb base folder. This is determined automatically when the PaintWeb + * script is added in a page. + * @type String + */ +PaintWeb.baseFolder = ''; + +(function () { + var scripts = document.getElementsByTagName('script'), + n = scripts.length, + pos, src; + + // Determine the baseFolder. + + for (var i = 0; i < n; i++) { + src = scripts[i].src; + if (!src || !/paintweb(\.dev|\.src)?\.js/.test(src)) { + continue; + } + + pos = src.lastIndexOf('/'); + if (pos !== -1) { + PaintWeb.baseFolder = src.substr(0, pos + 1); + } + + break; + } +})(); + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/bcurve.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/bcurve.js new file mode 100644 index 00000000..a4b7d41e --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/bcurve.js @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-08-24 13:18:05 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the Bézier curve tool implementation. + */ + +/** + * @class The Bézier curve tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.bcurve = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + image = app.image, + mouse = app.mouse, + setInterval = app.win.setInterval, + snapXY = app.toolSnapXY; + + /** + * Holds the points in the Bézier curve being drawn. + * + * @private + * @type Array + */ + var points = []; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * The tool deactivation method, used for clearing the buffer. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (points.length > 0) { + context.clearRect(0, 0, image.width, image.height); + } + + needsRedraw = false; + points = []; + + return true; + }; + + /** + * The mousedown event handler. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + if (points.length === 0) { + gui.statusShow('bcurveSnapping'); + points.push([mouse.x, mouse.y]); + } + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + + shiftKey = ev.shiftKey; + needsRedraw = false; + + return true; + }; + + /** + * Store the Shift key state which is used by the drawing function. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Draw the Bézier curve, using the available points. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + if (!needsRedraw) { + return; + } + + var n = points.length; + + // Add the temporary point while the mouse button is down. + if (mouse.buttonDown) { + if (shiftKey && n === 1) { + snapXY(points[0][0], points[0][1]); + } + points.push([mouse.x, mouse.y]); + n++; + } + + var p0 = points[0], + p1 = points[1], + p2 = points[2], + p3 = points[3] || points[2]; + + if (mouse.buttonDown) { + points.pop(); + } + + context.clearRect(0, 0, image.width, image.height); + + if (!n) { + needsRedraw = false; + return; + } + + // Draw the main line + if (n === 2) { + context.beginPath(); + context.moveTo(p0[0], p0[1]+2); + context.lineTo(p1[0], p1[1]+2); + + if (config.shapeType === 'fill') { + var lineWidth = context.lineWidth, + strokeStyle = context.strokeStyle; + + context.lineWidth = 1; + context.strokeStyle = context.fillStyle; + } + + context.stroke(); + context.closePath(); + + if (config.shapeType === 'fill') { + context.lineWidth = lineWidth; + context.strokeStyle = strokeStyle; + } + + needsRedraw = false; + return; + } + + // Draw the Bézier curve + + context.beginPath(); + context.moveTo(p0[0], p0[1]); + context.bezierCurveTo( + p2[0], p2[1], + p3[0], p3[1], + p1[0], p1[1]); + + if (config.shapeType !== 'stroke') { + context.fill(); + } + + if (config.shapeType !== 'fill') { + context.stroke(); + } + + context.closePath(); + + needsRedraw = false; + }; + + /** + * The mouseup event handler. This method stores the current + * mouse coordinates as a point to be used for drawing the Bézier curve. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + var n = points.length; + + // Allow click+mousemove+click, not only mousedown+mousemove+mouseup. + // Do this only for the start point. + if (n === 1 && mouse.x === points[0][0] && mouse.y === points[0][1]) { + mouse.buttonDown = true; + return true; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + if (n === 1 && ev.shiftKey) { + snapXY(points[0][0], points[0][1]); + } + + // We need 4 points to draw the Bézier curve: start, end, and two control + // points. + if (n < 4) { + points.push([mouse.x, mouse.y]); + needsRedraw = true; + n++; + } + + // Make sure the canvas is up-to-date. + shiftKey = ev.shiftKey; + _self.draw(); + + if (n === 2 || n === 3) { + gui.statusShow('bcurveControlPoint' + (n-1)); + } else if (n === 4) { + gui.statusShow('bcurveActive'); + app.layerUpdate(); + points = []; + } + + return true; + }; + + /** + * The keydown event handler. This method allows the user to + * press the Escape key to cancel the current drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + */ + this.keydown = function (ev) { + if (!points.length || ev.kid_ !== 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + context.clearRect(0, 0, image.width, image.height); + + points = []; + needsRedraw = false; + mouse.buttonDown = false; + + gui.statusShow('bcurveActive'); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cbucket.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cbucket.js new file mode 100644 index 00000000..0c408cb5 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cbucket.js @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-11-10 20:12:34 +0200 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the color bucket tool implementation, also known as the + * flood fill tool. + */ + +/** + * @class The color bucket tool. + * + * The implementation here is based on the seed fill algorithm of Paul S. + * Heckbert (1990). + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.cbucket = function (app) { + var _self = this, + config = app.config, + layer = app.layer.context, + buffer = app.buffer.context, + iwidth = app.image.width, + iheight = app.image.height, + mouse = app.mouse; + + var stackMax = 10000; // maximum depth of stack + var lines = []; // stack of lines + var pixelNew, layerpix; + + /** + * The preActivate event handler. This method checks if the + * browser implements the getImageData() and + * putImageData() context methods. If not, the color bucket tool + * cannot be used. + * + * @returns {Boolean} True if the drawing tool can be activated, or false + * otherwise. + */ + this.preActivate = function () { + // The latest versions of all browsers which implement Canvas, also + // implement the getImageData() method. This was only a problem with some + // old versions (eg. Opera 9.2). + if (!layer.getImageData || !layer.putImageData) { + alert(app.lang.errorCbucketUnsupported); + return false; + } else { + return true; + } + }; + + /** + * The activate event handler. Canvas shadow rendering is + * disabled. + */ + this.activate = function () { + app.shadowDisallow(); + }; + + /** + * The deactivate event handler. Canvas shadow rendering is + * allowed once again. + */ + this.deactivate = function () { + app.shadowAllow(); + }; + + /** + * The click and contextmenu event handler. This + * method performs the flood fill operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the image was modified, or false otherwise. + */ + this.click = function (ev) { + // Allow the user to right-click or hold down the Shift key to use the + // border color for filling the image. + if (ev.type === 'contextmenu' || ev.button === 2 || ev.shiftKey) { + var fillStyle = buffer.fillStyle; + buffer.fillStyle = buffer.strokeStyle; + buffer.fillRect(0, 0, 1, 1); + buffer.fillStyle = fillStyle; + } else { + buffer.fillRect(0, 0, 1, 1); + } + + // Instead of parsing the fillStyle ... + pixelNew = buffer.getImageData(0, 0, 1, 1); + pixelNew = [pixelNew.data[0], pixelNew.data[1], pixelNew.data[2], + pixelNew.data[3]]; + + buffer.clearRect(0, 0, 1, 1); + + var pixelOld = layer.getImageData(mouse.x, mouse.y, 1, 1).data; + pixelOld = pixelOld[0] + ';' + pixelOld[1] + ';' + pixelOld[2] + ';' + + pixelOld[3]; + + if (pixelOld === pixelNew.join(';')) { + return false; + } + + fill(mouse.x, mouse.y, pixelOld); + + app.historyAdd(); + + return true; + }; + this.contextmenu = this.click; + + /** + * Fill the image with the current fill color, starting from the x + * and y coordinates. + * + * @private + * + * @param {Number} x The x coordinate for the starting point. + * @param {Number} y The y coordinate for the starting point. + * @param {String} pixelOld The old pixel value. + */ + var fill = function (x, y, pixelOld) { + var start, x1, x2, dy, tmp, idata; + + pushLine(y, x, x, 1); // needed in some cases + pushLine(y + 1, x, x, -1); // seed segment (popped 1st) + + while (lines.length > 0) { + // pop segment off stack and fill a neighboring scan line + tmp = lines.pop(); + dy = tmp[3]; + y = tmp[0] + dy; + x1 = tmp[1]; + x2 = tmp[2]; + + layerpix = null; + idata = layer.getImageData(0, y, iwidth, 1); + layerpix = idata.data; + + // segment of scan line y-dy for x1 <= x <= x2 was previously filled, now + // explore adjacent pixels in scan line y + for (x = x1; x >= 0 && pixelRead(x) === pixelOld; x--) { + pixelWrite(x); + } + + if (x >= x1) { + for (x++; x <= x2 && pixelRead(x) !== pixelOld; x++); + start = x; + if (x > x2) { + layer.putImageData(idata, 0, y); + continue; + } + + } else { + start = x + 1; + if (start < x1) { + pushLine(y, start, x1 - 1, -dy); // leak on left? + } + + x = x1 + 1; + } + + do { + for (; x < iwidth && pixelRead(x) === pixelOld; x++) { + pixelWrite(x); + } + + pushLine(y, start, x - 1, dy); + if (x > (x2 + 1)) { + pushLine(y, x2 + 1, x - 1, -dy); // leak on right? + } + + for (x++; x <= x2 && pixelRead(x) !== pixelOld; x++); + start = x; + + } while (x <= x2); + + layer.putImageData(idata, 0, y); + } + + layerpix = null; + idata = null; + }; + + var pushLine = function (y, xl, xr, dy) { + if (lines.length < stackMax && (y+dy) >= 0 && (y+dy) < iheight) { + lines.push([y, xl, xr, dy]); + } + }; + + var pixelRead = function (x) { + var r = 4 * x; + return layerpix[r] + ';' + layerpix[r+1] + ';' + layerpix[r+2] + ';' + + layerpix[r+3]; + }; + + var pixelWrite = function (x) { + var r = 4 * x; + layerpix[r] = pixelNew[0]; + layerpix[r+1] = pixelNew[1]; + layerpix[r+2] = pixelNew[2]; + layerpix[r+3] = pixelNew[3]; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cpicker.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cpicker.js new file mode 100644 index 00000000..1788d417 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/cpicker.js @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-02 15:37:38 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the color picker implementation. + */ + +/** + * @class The color picker tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.cpicker = function (app) { + var _self = this, + colormixer = app.extensions.colormixer, + context = app.layer.context, + gui = app.gui, + lang = app.lang, + MathRound = Math.round, + mouse = app.mouse; + + /** + * Holds the ID of the previously active tool. Once the user completes the + * color picking operation, the previous tool is activated. + * + * @private + * @type String + */ + var prevTool = null; + + /** + * Holds a reference to the target color input. This is a GUI color input + * component. + * + * @private + * @type pwlib.guiColorInput + */ + var targetInput = null; + + /** + * Holds the previous color values - before the user started picking + * a different color. + * + * @private + * @type Object + */ + var prevColor = null; + + /** + * Tells if the color mixer is active for the current target input. + * + * @private + * @type Boolean + */ + var colormixerActive = false; + + /** + * Tells if the current color values are accepted by the user. This value is + * used by the tool deactivation code. + * + * @private + * @type Boolean + */ + var colorAccepted = false; + + /** + * The preActivate event handler. This method checks if the + * browser implements the getImageData() context method. If not, + * the color picker tool cannot be used. + */ + this.preActivate = function () { + // The latest versions of all browsers which implement Canvas, also + // implement the getImageData() method. This was only a problem with some + // old versions (eg. Opera 9.2). + if (!context.getImageData) { + alert(lang.errorCpickerUnsupported); + return false; + } + + if (app.tool && app.tool._id) { + prevTool = app.tool._id; + } + + return true; + }; + + /** + * The activate event handler. This method determines the current + * target input in the Color Mixer, if any. Canvas shadow rendering is + * disallowed. + */ + this.activate = function () { + // When the color mixer panel is active, the color picker uses the same + // target input. + if (colormixer && colormixer.targetInput) { + targetInput = gui.colorInputs[colormixer.targetInput.id]; + } + + if (targetInput) { + gui.statusShow('cpicker_' + targetInput.id); + } else { + gui.statusShow('cpickerNormal'); + } + + app.shadowDisallow(); + }; + + /** + * The deactivate event handler. This method allows shadow + * rendering again, and resets the color input values if the user did not + * accept the new color. + */ + this.deactivate = function () { + if (!colorAccepted && targetInput && prevColor) { + updateColor(null, true); + } + + app.shadowAllow(); + }; + + /** + * The mousedown event handler. This method starts the color + * picking operation. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + // We check again, because the user might have opened/closed the color + // mixer. + if (colormixer && colormixer.targetInput) { + targetInput = gui.colorInputs[colormixer.targetInput.id]; + } + + if (targetInput) { + colormixerActive = true; + gui.statusShow('cpicker_' + targetInput.id); + } else { + colormixerActive = false; + gui.statusShow('cpickerNormal'); + + // The context menu (right-click). This is unsupported by Opera. + // Also allow Shift+Click for changing the stroke color (making it easier for Opera users). + if (ev.button === 2 || ev.shiftKey) { + targetInput = gui.colorInputs.strokeStyle; + } else { + targetInput = gui.colorInputs.fillStyle; + } + } + + updatePrevColor(); + + _self.mousemove = updateColor; + updateColor(ev); + + return true; + }; + + /** + * Perform color update. This function updates the target input or the Color + * Mixer to hold the color value under the mouse - it actually performs the + * color picking operation. + * + *

This function is also the mousemove event handler for this + * tool. + * + * @param {Event} ev The DOM Event object. + * @param {Boolean} [usePrevColor=false] Tells the function to use the + * previous color values we have stored. This is used when the user cancels + * the color picking operation. + */ + function updateColor (ev, usePrevColor) { + if (!targetInput) { + return; + } + + var p = usePrevColor ? prevColor : + context.getImageData(mouse.x, mouse.y, 1, 1), + color = { + red: p.data[0] / 255, + green: p.data[1] / 255, + blue: p.data[2] / 255, + alpha: (p.data[3] / 255).toFixed(3) + }; + + if (colormixerActive) { + colormixer.color.red = color.red; + colormixer.color.green = color.green; + colormixer.color.blue = color.blue; + colormixer.color.alpha = color.alpha; + colormixer.update_color('rgb'); + + } else { + targetInput.updateColor(color); + } + }; + + /** + * The mouseup event handler. This method completes the color + * picking operation, and activates the previous tool. + * + *

The {@link pwlib.appEvent.configChange} application event is also + * dispatched for the configuration property associated to the target input. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + if (!targetInput) { + return false; + } + + delete _self.mousemove; + updateColor(ev); + colorAccepted = true; + + if (!colormixerActive) { + var color = targetInput.color, + configProperty = targetInput.configProperty, + configGroup = targetInput.configGroup, + configGroupRef = targetInput.configGroupRef, + prevVal = configGroupRef[configProperty], + newVal = 'rgba(' + MathRound(color.red * 255) + ',' + + MathRound(color.green * 255) + ',' + + MathRound(color.blue * 255) + ',' + + color.alpha + ')'; + + if (prevVal !== newVal) { + configGroupRef[configProperty] = newVal; + app.events.dispatch(new pwlib.appEvent.configChange(newVal, prevVal, + configProperty, configGroup, configGroupRef)); + } + } + + if (prevTool) { + app.toolActivate(prevTool, ev); + } + + return true; + }; + + /** + * The keydown event handler. This method allows the user to + * press the Escape key to cancel the color picking operation. By + * doing so, the original color values are restored. + * + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + */ + this.keydown = function (ev) { + if (!prevTool || ev.kid_ !== 'Escape') { + return false; + } + + mouse.buttonDown = false; + app.toolActivate(prevTool, ev); + + return true; + }; + + /** + * The contextmenu event handler. This method only cancels the + * context menu. + */ + // Unfortunately, the contextmenu event is unsupported by Opera. + this.contextmenu = function () { + return true; + }; + + /** + * Store the color values from the target color input, before this tool + * changes the colors. The previous color values are used when the user + * decides to cancel the color picking operation. + * @private + */ + function updatePrevColor () { + // If the color mixer panel is visible, then we store the color values from + // the color mixer, instead of those from the color input object. + var color = colormixerActive ? colormixer.color : targetInput.color; + + prevColor = { + width: 1, + height: 1, + data: [ + MathRound(color.red * 255), + MathRound(color.green * 255), + MathRound(color.blue * 255), + color.alpha * 255 + ] + }; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/ellipse.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/ellipse.js new file mode 100644 index 00000000..19bb6a93 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/ellipse.js @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-01 18:44:56 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the ellipse tool implementation. + */ + +/** + * @class The ellipse tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.ellipse = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + image = app.image, + MathMax = Math.max, + MathMin = Math.min, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + var K = 4*((Math.SQRT2-1)/3); + + /** + * Holds the starting point on the x axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Tool deactivation event handler. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (mouse.buttonDown) { + context.clearRect(0, 0, image.width, image.height); + } + + needsRedraw = false; + + return true; + }; + + /** + * Initialize the drawing operation. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + // The mouse start position + x0 = mouse.x; + y0 = mouse.y; + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + shiftKey = ev.shiftKey; + needsRedraw = false; + + gui.statusShow('ellipseMousedown'); + + return true; + }; + + /** + * Store the Shift key state which is used by the drawing function. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Perform the drawing operation. This function is called every few + * milliseconds. + * + *

Hold down the Shift key to draw a circle. + *

Press Escape to cancel the drawing operation. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + if (!needsRedraw) { + return; + } + + context.clearRect(0, 0, image.width, image.height); + + var rectx0 = MathMin(mouse.x, x0), + rectx1 = MathMax(mouse.x, x0), + recty0 = MathMin(mouse.y, y0), + recty1 = MathMax(mouse.y, y0); + + /* + ABCD - rectangle + A(rectx0, recty0), B(rectx1, recty0), C(rectx1, recty1), D(rectx0, recty1) + */ + + var w = rectx1-rectx0, + h = recty1-recty0; + + if (!w || !h) { + needsRedraw = false; + return; + } + + // Constrain the ellipse to be a circle + if (shiftKey) { + if (w > h) { + recty1 = recty0+w; + if (recty0 == mouse.y) { + recty0 -= w-h; + recty1 -= w-h; + } + h = w; + } else { + rectx1 = rectx0+h; + if (rectx0 == mouse.x) { + rectx0 -= h-w; + rectx1 -= h-w; + } + w = h; + } + } + + // Ellipse radius + var rx = w/2, + ry = h/2; + + // Ellipse center + var cx = rectx0+rx, + cy = recty0+ry; + + // Ellipse radius*Kappa, for the Bézier curve control points + rx *= K; + ry *= K; + + context.beginPath(); + + // startX, startY + context.moveTo(cx, recty0); + + // Control points: cp1x, cp1y, cp2x, cp2y, destx, desty + // go clockwise: top-middle, right-middle, bottom-middle, then left-middle + context.bezierCurveTo(cx + rx, recty0, rectx1, cy - ry, rectx1, cy); + context.bezierCurveTo(rectx1, cy + ry, cx + rx, recty1, cx, recty1); + context.bezierCurveTo(cx - rx, recty1, rectx0, cy + ry, rectx0, cy); + context.bezierCurveTo(rectx0, cy - ry, cx - rx, recty0, cx, recty0); + + if (config.shapeType != 'stroke') { + context.fill(); + } + if (config.shapeType != 'fill') { + context.stroke(); + } + + context.closePath(); + + needsRedraw = false; + }; + + /** + * End the drawing operation, once the user releases the mouse button. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + // Allow click+mousemove, not only mousedown+move+up + if (mouse.x == x0 && mouse.y == y0) { + mouse.buttonDown = true; + return true; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + shiftKey = ev.shiftKey; + _self.draw(); + app.layerUpdate(); + gui.statusShow('ellipseActive'); + + return true; + }; + + /** + * Allows the user to press Escape to cancel the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the drawing operation was cancelled, or false if + * not. + */ + this.keydown = function (ev) { + if (!mouse.buttonDown || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + context.clearRect(0, 0, image.width, image.height); + mouse.buttonDown = false; + needsRedraw = false; + + gui.statusShow('ellipseActive'); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/eraser.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/eraser.js new file mode 100644 index 00000000..5dd85863 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/eraser.js @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-29 20:34:06 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the eraser tool implementation. + */ + +/** + * @class The eraser tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.eraser = function (app) { + var _self = this, + bufferContext = app.buffer.context, + clearInterval = app.win.clearInterval, + config = app.config, + history = app.history.pos, + image = app.image, + layerContext = app.layer.context, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * The interval ID used for running the erasing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Holds the points needed to be drawn. Each point is added by the + * mousemove event handler. + * + * @private + * @type Array + */ + var points = []; + + /** + * Holds the starting point on the x axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + var globalOp_ = null, + lineWidth_ = null; + + /** + * The tool deactivation event handler. This function clears timers, clears + * the canvas and allows shadows to be rendered again. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (mouse.buttonDown) { + if (globalOp_) { + layerContext.globalCompositeOperation = globalOp_; + } + if (lineWidth_) { + layerContext.lineWidth = lineWidth_; + } + + app.historyGoto(history.pos); + } + + points = []; + + // Allow Canvas shadows. + app.shadowAllow(); + }; + + /** + * The tool activation event handler. This is run after the tool construction + * and after the deactivation of the previous tool. This function simply + * disallows the rendering of shadows. + */ + this.activate = function () { + // Do not allow Canvas shadows. + app.shadowDisallow(); + }; + + /** + * Initialize the drawing operation. + */ + this.mousedown = function () { + globalOp_ = layerContext.globalCompositeOperation; + lineWidth_ = layerContext.lineWidth; + + layerContext.globalCompositeOperation = 'destination-out'; + layerContext.lineWidth = bufferContext.lineWidth; + + x0 = mouse.x; + y0 = mouse.y; + + points = []; + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + + return true; + }; + + /** + * Save the mouse coordinates in the array. + */ + this.mousemove = function () { + if (mouse.buttonDown) { + points.push(mouse.x, mouse.y); + } + }; + + /** + * Draw the points in the stack. This function is called every few + * milliseconds. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + var i = 0, n = points.length; + if (!n) { + return; + } + + layerContext.beginPath(); + layerContext.moveTo(x0, y0); + + while (i < n) { + x0 = points[i++]; + y0 = points[i++]; + layerContext.lineTo(x0, y0); + } + + layerContext.stroke(); + layerContext.closePath(); + + points = []; + }; + + /** + * End the drawing operation, once the user releases the mouse button. + */ + this.mouseup = function () { + if (mouse.x == x0 && mouse.y == y0) { + points.push(x0+1, y0+1); + } + + if (timer) { + clearInterval(timer); + timer = null; + } + _self.draw(); + + layerContext.globalCompositeOperation = globalOp_; + layerContext.lineWidth = lineWidth_; + + app.historyAdd(); + + return true; + }; + + /** + * Allows the user to press Escape to cancel the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the drawing operation was cancelled, or false if + * not. + */ + this.keydown = function (ev) { + if (!mouse.buttonDown || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + layerContext.globalCompositeOperation = globalOp_; + layerContext.lineWidth = lineWidth_; + + mouse.buttonDown = false; + points = []; + + app.historyGoto(history.pos); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/hand.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/hand.js new file mode 100644 index 00000000..88f912e5 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/hand.js @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-15 20:27:08 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the hand tool implementation. + */ + +/** + * @class The hand tool. This tool allows the user to drag the image canvas + * inside the viewport. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.hand = function (app) { + var _self = this, + bufferCanvas = app.buffer.canvas, + bufferStyle = bufferCanvas.style, + config = app.config; + clearInterval = app.win.clearInterval, + image = app.image, + MathRound = Math.round, + mouse = app.mouse, + viewport = app.gui.elems.viewport, + vheight = 0, + vwidth = 0, + setInterval = app.win.setInterval; + + /** + * The interval ID used for invoking the viewport drag operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the viewport needs to be scrolled. + * + * @private + * @type Boolean + * @default false + */ + var needsScroll = false; + + /** + * Holds the previous tool ID. + * + * @private + * @type String + */ + this.prevTool = null; + + var x0 = 0, y0 = 0, + x1 = 0, y1 = 0, + l0 = 0, t0 = 0; + + /** + * Tool preactivation event handler. + * + * @returns {Boolean} True if the tool can become active, or false if not. + */ + this.preActivate = function () { + if (!viewport) { + return false; + } + + _self.prevTool = app.tool._id; + + // Check if the image canvas can be scrolled within the viewport. + + var cs = app.win.getComputedStyle(viewport, null), + bwidth = parseInt(bufferStyle.width), + bheight = parseInt(bufferStyle.height); + + vwidth = parseInt(cs.width), + vheight = parseInt(cs.height); + + if (vheight < bheight || vwidth < bwidth) { + return true; + } else { + return false; + } + }; + + /** + * Tool activation event handler. + */ + this.activate = function () { + bufferStyle.cursor = 'move'; + app.shadowDisallow(); + }; + + /** + * Tool deactivation event handler. + */ + this.deactivate = function (ev) { + if (timer) { + clearInterval(timer); + timer = null; + app.doc.removeEventListener('mousemove', ev_mousemove, false); + app.doc.removeEventListener('mouseup', ev_mouseup, false); + } + + bufferStyle.cursor = ''; + app.shadowAllow(); + }; + + /** + * Initialize the canvas drag. + * + * @param {Event} ev The DOM event object. + */ + this.mousedown = function (ev) { + x0 = ev.clientX; + y0 = ev.clientY; + l0 = viewport.scrollLeft; + t0 = viewport.scrollTop; + + needsScroll = false; + + app.doc.addEventListener('mousemove', ev_mousemove, false); + app.doc.addEventListener('mouseup', ev_mouseup, false); + + if (!timer) { + timer = setInterval(viewportScroll, config.toolDrawDelay); + } + + return true; + }; + + /** + * The mousemove event handler. This simply stores the current + * mouse location. + * + * @param {Event} ev The DOM Event object. + */ + function ev_mousemove (ev) { + x1 = ev.clientX; + y1 = ev.clientY; + needsScroll = true; + }; + + /** + * Perform the canvas drag operation. This function is called every few + * milliseconds. + * + *

Press Escape to stop dragging and to get back to the previous + * tool. + */ + function viewportScroll () { + if (needsScroll) { + viewport.scrollTop = t0 - y1 + y0; + viewport.scrollLeft = l0 - x1 + x0; + needsScroll = false; + } + }; + + /** + * The mouseup event handler. + */ + function ev_mouseup (ev) { + if (timer) { + clearInterval(timer); + timer = null; + } + + ev_mousemove(ev); + viewportScroll(); + + app.doc.removeEventListener('mousemove', ev_mousemove, false); + app.doc.removeEventListener('mouseup', ev_mouseup, false); + + mouse.buttonDown = false; + }; + + /** + * Allows the user to press Escape to stop dragging the canvas, and + * to return to the previous tool. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the key was recognized, or false if not. + */ + this.keydown = function (ev) { + if (!_self.prevTool || ev.kid_ != 'Escape') { + return false; + } + + app.toolActivate(_self.prevTool, ev); + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/insertimg.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/insertimg.js new file mode 100644 index 00000000..2c67e83e --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/insertimg.js @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-06 16:20:38 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the "Insert image" tool implementation. + */ + +// TODO: allow inserting images from a different host, using server-side magic. + +/** + * @class The "Insert image" tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.insertimg = function (app) { + var _self = this, + canvasImage = app.image, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + lang = app.lang, + MathAbs = Math.abs, + MathMin = Math.min, + MathRound = Math.round, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * Holds the previous tool ID. + * + * @private + * @type String + */ + var prevTool = app.tool ? app.tool._id : null; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * Holds the starting point on the x axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Tells if the image element loaded or not. + * + * @private + * @type Boolean + */ + var imageLoaded = false; + + /** + * Holds the image aspect ratio, used by the resize method. + * + * @private + * @type Number + */ + var imageRatio = 1; + + /** + * Holds the DOM image element. + * + * @private + * @type Element + */ + var imageElement = null; + + /** + * Holds the image address. + * @type String + */ + if (!this.url) { + this.url = 'http://'; + } + + /** + * The tool preactivation code. This function asks the user to provide an URL + * to the image which is desired to be inserted into the canvas. + * + * @returns {Boolean} True if the URL provided is correct. False is returned + * if the URL is not provided or if it's incorrect. When false is returned the + * tool activation is cancelled. + */ + this.preActivate = function () { + if (!gui.elems.viewport) { + return false; + } + + _self.url = prompt(lang.promptInsertimg, _self.url); + + if (!_self.url || _self.url.toLowerCase() === 'http://') { + return false; + } + + // Remember the URL. + pwlib.extend(true, _self.constructor.prototype, {url: _self.url}); + + if (!pwlib.isSameHost(_self.url, app.win.location.host)) { + alert(lang.errorInsertimgHost); + return false; + } + + return true; + }; + + /** + * The tool activation event handler. This function is called once the + * previous tool has been deactivated. + */ + this.activate = function () { + imageElement = new Image(); + imageElement.addEventListener('load', ev_imageLoaded, false); + imageElement.src = _self.url; + + return true; + }; + + /** + * The tool deactivation event handler. + */ + this.deactivate = function () { + if (imageElement) { + imageElement = null; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + needsRedraw = false; + + context.clearRect(0, 0, canvasImage.width, canvasImage.height); + + return true; + }; + + /** + * The load event handler for the image element. This method + * makes sure the image dimensions are synchronized with the zoom level, and + * draws the image on the canvas. + * + * @private + */ + function ev_imageLoaded () { + // Did the image already load? + if (imageLoaded) { + return; + } + + // The default position for the inserted image is the top left corner of the visible area, taking into consideration the zoom level. + var x = MathRound(gui.elems.viewport.scrollLeft / canvasImage.canvasScale), + y = MathRound(gui.elems.viewport.scrollTop / canvasImage.canvasScale); + + context.clearRect(0, 0, canvasImage.width, canvasImage.height); + + try { + context.drawImage(imageElement, x, y); + } catch (err) { + alert(lang.errorInsertimg); + return; + } + + imageLoaded = true; + needsRedraw = false; + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + + gui.statusShow('insertimgLoaded'); + }; + + /** + * The mousedown event handler. This method stores the current + * mouse location and the image aspect ratio for later reuse by the + * draw() method. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + if (!imageLoaded) { + alert(lang.errorInsertimgNotLoaded); + return false; + } + + x0 = mouse.x; + y0 = mouse.y; + + // The image aspect ratio - used by the draw() method when the user holds + // the Shift key down. + imageRatio = imageElement.width / imageElement.height; + shiftKey = ev.shiftKey; + + gui.statusShow('insertimgResize'); + + if (ev.stopPropagation) { + ev.stopPropagation(); + } + }; + + /** + * The mousemove event handler. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Perform the drawing operation. When the mouse button is not down, the user + * is allowed to pick where he/she wants to insert the image element, inside + * the canvas. Once the mousedown event is fired, this method + * allows the user to resize the image inside the canvas. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + if (!imageLoaded || !needsRedraw) { + return; + } + + context.clearRect(0, 0, canvasImage.width, canvasImage.height); + + // If the user is holding down the mouse button, then allow him/her to + // resize the image. + if (mouse.buttonDown) { + var w = MathAbs(mouse.x - x0), + h = MathAbs(mouse.y - y0), + x = MathMin(mouse.x, x0), + y = MathMin(mouse.y, y0); + + if (!w || !h) { + needsRedraw = false; + return; + } + + // If the Shift key is down, constrain the image to have the same aspect + // ratio as the original image element. + if (shiftKey) { + if (w > h) { + if (y == mouse.y) { + y -= w-h; + } + h = MathRound(w/imageRatio); + } else { + if (x == mouse.x) { + x -= h-w; + } + w = MathRound(h*imageRatio); + } + } + + context.drawImage(imageElement, x, y, w, h); + } else { + // If the mouse button is not down, simply allow the user to pick where + // he/she wants to insert the image element. + context.drawImage(imageElement, mouse.x, mouse.y); + } + + needsRedraw = false; + }; + + /** + * The mouseup event handler. This method completes the drawing + * operation by inserting the image in the layer canvas, and by activating the + * previous tool. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + if (!imageLoaded) { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + app.layerUpdate(); + + if (prevTool) { + app.toolActivate(prevTool, ev); + } + + if (ev.stopPropagation) { + ev.stopPropagation(); + } + }; + + /** + * The keydown event handler allows users to press the + * Escape key to cancel the drawing operation and return to the + * previous tool. + * + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the key was recognized, or false if not. + */ + this.keydown = function (ev) { + if (!prevTool || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + mouse.buttonDown = false; + app.toolActivate(prevTool, ev); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/line.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/line.js new file mode 100644 index 00000000..a00ba5ce --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/line.js @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-11 20:23:04 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the line tool implementation. + */ + +/** + * @class The line tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.line = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + image = app.image, + mouse = app.mouse, + setInterval = app.win.setInterval, + snapXY = app.toolSnapXY; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * Holds the starting point on the x axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Tool deactivation event handler. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (mouse.buttonDown) { + context.clearRect(0, 0, image.width, image.height); + } + + needsRedraw = false; + + return true; + }; + + /** + * Initialize the drawing operation, by storing the location of the pointer, + * the start position. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + x0 = mouse.x; + y0 = mouse.y; + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + shiftKey = ev.shiftKey; + needsRedraw = false; + + gui.statusShow('lineMousedown'); + + return true; + }; + + /** + * Store the Shift key state which is used by the drawing function. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Perform the drawing operation. This function is called every few + * milliseconds. + * + *

Hold down the Shift key to draw a straight + * horizontal/vertical line. + *

Press Escape to cancel the drawing operation. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + if (!needsRedraw) { + return; + } + + context.clearRect(0, 0, image.width, image.height); + + // Snapping on the X/Y axis. + if (shiftKey) { + snapXY(x0, y0); + } + + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(mouse.x, mouse.y); + context.stroke(); + context.closePath(); + + needsRedraw = false; + }; + + /** + * End the drawing operation, once the user releases the mouse button. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + // Allow users to click then drag, not only mousedown+drag+mouseup. + if (mouse.x == x0 && mouse.y == y0) { + mouse.buttonDown = true; + return true; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + shiftKey = ev.shiftKey; + _self.draw(); + gui.statusShow('lineActive'); + app.layerUpdate(); + + return true; + }; + + /** + * Allows the user to press Escape to cancel the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the drawing operation was cancelled, or false if + * not. + */ + this.keydown = function (ev) { + if (!mouse.buttonDown || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + context.clearRect(0, 0, image.width, image.height); + mouse.buttonDown = false; + needsRedraw = false; + + gui.statusShow('lineActive'); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/pencil.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/pencil.js new file mode 100644 index 00000000..ebb9472d --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/pencil.js @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-15 15:25:29 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the pencil tool implementation. + */ + +/** + * @class The drawing pencil. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.pencil = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + context = app.buffer.context, + image = app.image, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * The interval ID used for running the pencil drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Holds the points needed to be drawn. Each point is added by the + * mousemove event handler. + * + * @private + * @type Array + */ + var points = []; + + /** + * Holds the last point on the x axis of the image, for the current + * drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the last point on the y axis of the image, for the current + * drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Tool deactivation event handler. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (mouse.buttonDown) { + context.clearRect(0, 0, image.width, image.height); + } + + points = []; + }; + + /** + * Initialize the drawing operation. + */ + this.mousedown = function () { + x0 = mouse.x; + y0 = mouse.y; + + points = []; + if (!timer) { + timer = setInterval(_self.draw, app.config.toolDrawDelay); + } + + return true; + }; + + /** + * Save the mouse coordinates in the array. + */ + this.mousemove = function () { + if (mouse.buttonDown) { + points.push(mouse.x, mouse.y); + } + }; + + /** + * Draw the points in the stack. This function is called every few + * milliseconds. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + var i = 0, n = points.length; + if (!n) { + return; + } + + context.beginPath(); + context.moveTo(x0, y0); + + while (i < n) { + x0 = points[i++]; + y0 = points[i++]; + context.lineTo(x0, y0); + } + + context.stroke(); + context.closePath(); + + points = []; + }; + + /** + * End the drawing operation, once the user releases the mouse button. + */ + this.mouseup = function () { + if (mouse.x == x0 && mouse.y == y0) { + points.push(x0+1, y0+1); + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + _self.draw(); + app.layerUpdate(); + + return true; + }; + + /** + * Allows the user to press Escape to cancel the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the drawing operation was cancelled, or false if + * not. + */ + this.keydown = function (ev) { + if (!mouse.buttonDown || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + context.clearRect(0, 0, image.width, image.height); + mouse.buttonDown = false; + points = []; + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/polygon.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/polygon.js new file mode 100644 index 00000000..178fec18 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/polygon.js @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-11 20:28:07 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the polygon tool implementation. + */ + +/** + * @class The polygon tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.polygon = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + image = app.image, + MathAbs = Math.abs, + mouse = app.mouse, + setInterval = app.win.setInterval, + snapXY = app.toolSnapXY; + + /** + * Holds the points in the polygon being drawn. + * + * @private + * @type Array + */ + var points = []; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * The tool deactivation method, used for clearing the buffer. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (points.length) { + context.clearRect(0, 0, image.width, image.height); + } + + needsRedraw = false; + points = []; + + return true; + }; + + /** + * The mousedown event handler. + * + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the event handler executed, or false if not. + */ + this.mousedown = function (ev) { + if (points.length == 0) { + points.push([mouse.x, mouse.y]); + } + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + + shiftKey = ev.shiftKey; + needsRedraw = false; + + gui.statusShow('polygonMousedown'); + + return true; + }; + + /** + * Store the Shift key state which is used by the drawing function. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Draw the polygon. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function (ev) { + if (!needsRedraw) { + return; + } + + var n = points.length; + + if (!n || (n == 1 && !mouse.buttonDown)) { + needsRedraw = false; + return; + } + + // Snapping on the X/Y axis for the current point (if available). + if (mouse.buttonDown && shiftKey) { + snapXY(points[n-1][0], points[n-1][1]); + } + + context.clearRect(0, 0, image.width, image.height); + context.beginPath(); + context.moveTo(points[0][0], points[0][1]); + + // Draw the path of the polygon + for (var i = 0; i < n; i++) { + context.lineTo(points[i][0], points[i][1]); + } + + if (mouse.buttonDown) { + context.lineTo(mouse.x, mouse.y); + } + + if (config.shapeType != 'stroke') { + context.fill(); + } + + // In the case where we only have a straight line, draw a stroke even if no + // stroke should be drawn, such that the user has better visual feedback. + if (config.shapeType != 'fill' || n == 1) { + context.stroke(); + } + + context.closePath(); + + needsRedraw = false; + }; + + /** + * The mouseup event handler. + * + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the event handler executed, or false if not. + */ + this.mouseup = function (ev) { + var n = points.length; + + // Allow click+mousemove+click, not only mousedown+mousemove+mouseup. + // Do this only for the first point in the polygon. + if (n == 1 && mouse.x == points[n-1][0] && mouse.y == points[n-1][1]) { + mouse.buttonDown = true; + return true; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + shiftKey = ev.shiftKey; + needsRedraw = true; + + if (ev.shiftKey) { + snapXY(points[n-1][0], points[n-1][1]); + } + + var diffx1 = MathAbs(mouse.x - points[0][0]), + diffy1 = MathAbs(mouse.y - points[0][1]), + diffx2 = MathAbs(mouse.x - points[n-1][0]), + diffy2 = MathAbs(mouse.y - points[n-1][1]); + + // End the polygon if the new point is close enough to the first/last point. + if ((diffx1 < 5 && diffy1 < 5) || (diffx2 < 5 && diffy2 < 5)) { + // Add the start point to complete the polygon shape. + points.push(points[0]); + + _self.draw(); + points = []; + + gui.statusShow('polygonActive'); + app.layerUpdate(); + + return true; + } + + if (n > 3) { + gui.statusShow('polygonEnd'); + } else { + gui.statusShow('polygonAddPoint'); + } + + points.push([mouse.x, mouse.y]); + _self.draw(); + + return true; + }; + + /** + * The keydown event handler. This method allows the user to + * cancel drawing the current polygon, using the Escape key. The + * Enter key can be used to accept the current polygon shape, and + * end the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + */ + this.keydown = function (ev) { + var n = points.length; + if (!n || (ev.kid_ != 'Escape' && ev.kid_ != 'Enter')) { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + mouse.buttonDown = false; + + if (ev.kid_ == 'Escape') { + context.clearRect(0, 0, image.width, image.height); + needsRedraw = false; + + } else if (ev.kid_ == 'Enter') { + // Add the point of the last mousemove event, and the start point, to + // complete the polygon. + points.push([mouse.x, mouse.y]); + points.push(points[0]); + needsRedraw = true; + _self.draw(); + app.layerUpdate(); + } + + points = []; + gui.statusShow('polygonActive'); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/rectangle.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/rectangle.js new file mode 100644 index 00000000..28cfaba1 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/rectangle.js @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-06-11 20:21:13 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the rectangle tool implementation. + */ + +/** + * @class The rectangle tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.rectangle = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config, + context = app.buffer.context, + gui = app.gui, + image = app.image, + MathAbs = Math.abs, + MathMin = Math.min, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the Shift key is down or not. This is used by the + * drawing function. + * + * @private + * @type Boolean + * @default false + */ + var shiftKey = false; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * Holds the starting point on the x axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the + * current drawing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Tool deactivation event handler. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + if (mouse.buttonDown) { + context.clearRect(0, 0, image.width, image.height); + } + + needsRedraw = false; + }; + + /** + * Initialize the drawing operation. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + x0 = mouse.x; + y0 = mouse.y; + + if (!timer) { + timer = setInterval(_self.draw, config.toolDrawDelay); + } + shiftKey = ev.shiftKey; + needsRedraw = false; + + gui.statusShow('rectangleMousedown'); + + return true; + }; + + /** + * Store the Shift key state which is used by the drawing function. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * Perform the drawing operation. This function is called every few + * milliseconds. + * + *

Hold down the Shift key to draw a square. + *

Press Escape to cancel the drawing operation. + * + * @see PaintWeb.config.toolDrawDelay + */ + this.draw = function () { + if (!needsRedraw) { + return; + } + + context.clearRect(0, 0, image.width, image.height); + + var x = MathMin(mouse.x, x0), + y = MathMin(mouse.y, y0), + w = MathAbs(mouse.x - x0), + h = MathAbs(mouse.y - y0); + + if (!w || !h) { + needsRedraw = false; + return; + } + + // Constrain the shape to a square + if (shiftKey) { + if (w > h) { + if (y == mouse.y) { + y -= w-h; + } + h = w; + } else { + if (x == mouse.x) { + x -= h-w; + } + w = h; + } + } + + if (config.shapeType != 'stroke') { + context.fillRect(x, y, w, h); + } + + if (config.shapeType != 'fill') { + context.strokeRect(x, y, w, h); + } + + needsRedraw = false; + }; + + /** + * End the drawing operation, once the user releases the mouse button. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + // Allow click+mousemove, not only mousedown+move+up + if (mouse.x == x0 && mouse.y == y0) { + mouse.buttonDown = true; + return true; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + shiftKey = ev.shiftKey; + _self.draw(); + app.layerUpdate(); + gui.statusShow('rectangleActive'); + + return true; + }; + + /** + * Allows the user to press Escape to cancel the drawing operation. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the drawing operation was cancelled, or false if + * not. + */ + this.keydown = function (ev) { + if (!mouse.buttonDown || ev.kid_ != 'Escape') { + return false; + } + + if (timer) { + clearInterval(timer); + timer = null; + } + + context.clearRect(0, 0, image.width, image.height); + mouse.buttonDown = false; + needsRedraw = false; + + gui.statusShow('rectangleActive'); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/selection.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/selection.js new file mode 100644 index 00000000..a211121c --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/selection.js @@ -0,0 +1,1505 @@ +/* + * Copyright (C) 2008, 2009 Mihai Şucan + * + * This file is part of PaintWeb. + * + * PaintWeb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PaintWeb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PaintWeb. If not, see . + * + * $URL: http://code.google.com/p/paintweb $ + * $Date: 2009-07-02 16:07:14 +0300 $ + */ + +/** + * @author Mihai Şucan + * @fileOverview Holds the selection tool implementation. + */ + +/** + * @class The selection tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.selection = function (app) { + var _self = this, + appEvent = pwlib.appEvent, + bufferContext = app.buffer.context, + clearInterval = app.win.clearInterval, + config = app.config.selection, + gui = app.gui, + image = app.image, + lang = app.lang, + layerCanvas = app.layer.canvas, + layerContext = app.layer.context, + marqueeStyle = null, + MathAbs = Math.abs, + MathMin = Math.min, + MathRound = Math.round, + mouse = app.mouse, + setInterval = app.win.setInterval, + snapXY = app.toolSnapXY; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var needsRedraw = false; + + /** + * The selection has been dropped, and the mouse button is down. The user has + * two choices: he releases the mouse button, thus the selection is dropped + * and the tool switches to STATE_NONE, or he moves the mouse in order to + * start a new selection (STATE_DRAWING). + * @constant + */ + this.STATE_PENDING = -1; + + /** + * No selection is available. + * @constant + */ + this.STATE_NONE = 0; + + /** + * The user is drawing a selection. + * @constant + */ + this.STATE_DRAWING = 1; + + /** + * The selection rectangle is available. + * @constant + */ + this.STATE_SELECTED = 2; + + /** + * The user is dragging/moving the selection rectangle. + * @constant + */ + this.STATE_DRAGGING = 3; + + /** + * The user is resizing the selection rectangle. + * @constant + */ + this.STATE_RESIZING = 4; + + /** + * Selection state. Known states: + * + *

    + *
  • {@link pwlib.tools.selection#STATE_PENDING} - Selection dropped after + * the mousedown event is fired. The script can switch to + * STATE_DRAWING if the mouse moves, or to STATE_NONE if it does not + * (allowing the user to drop the selection). + * + *
  • {@link pwlib.tools.selection#STATE_NONE} - No selection is available. + * + *
  • {@link pwlib.tools.selection#STATE_DRAWING} - The user is drawing the + * selection rectangle. + * + *
  • {@link pwlib.tools.selection#STATE_SELECTED} - The selection + * rectangle is available. + * + *
  • {@link pwlib.tools.selection#STATE_DRAGGING} - The user is + * dragging/moving the current selection. + * + *
  • {@link pwlib.tools.selection#STATE_RESIZING} - The user is resizing + * the current selection. + *
+ * + * @type Number + * @default STATE_NONE + */ + this.state = this.STATE_NONE; + + /** + * Holds the starting point on the x axis of the image, for any + * ongoing operation. + * + * @private + * @type Number + */ + var x0 = 0; + + /** + * Holds the starting point on the y axis of the image, for the any + * ongoing operation. + * + * @private + * @type Number + */ + var y0 = 0; + + /** + * Holds selection information and image. + * @type Object + */ + this.selection = { + /** + * Selection start point, on the x axis. + * @type Number + */ + x: 0, + + /** + * Selection start point, on the y axis. + * @type Number + */ + y: 0, + + /** + * Selection width. + * @type Number + */ + width: 0, + + /** + * Selection height. + * @type Number + */ + height: 0, + + /** + * Selection original width. The user can make a selection rectangle of + * a given width and height, but after that he/she can resize the selection. + * @type Number + */ + widthOriginal: 0, + + /** + * Selection original height. The user can make a selection rectangle of + * a given width and height, but after that he/she can resize the selection. + * @type Number + */ + heightOriginal: 0, + + /** + * Tells if the selected ImageData has been cut out or not from the + * layerContext. + * + * @type Boolean + * @default false + */ + layerCleared: false, + + /** + * Selection marquee/border element. + * @type HTMLElement + */ + marquee: null, + + /** + * Selection buffer context which holds the selected pixels. + * @type CanvasRenderingContext2D + */ + context: null, + + /** + * Selection buffer canvas which holds the selected pixels. + * @type HTMLCanvasElement + */ + canvas: null + }; + + /** + * The area type under the current mouse location. + * + *

When the selection is available the mouse location can be on top/inside + * the selection rectangle, on the border of the selection, or outside the + * selection. + * + *

Possible values: 'in', 'out', 'border'. + * + * @private + * @type String + * @default 'out' + */ + var mouseArea = 'out'; + + /** + * The resize type. If the mouse is on top of the selection border, then the + * selection can be resized. The direction of the resize operation is + * determined by the location of the mouse. + * + *

While the user resizes the selection this variable can hold the + * following values: 'n' (North), 'ne' (North-East), 'e' (East), 'se' + * (South-East), 's' (South), 'sw' (South-West), 'w' (West), 'nw' + * (North-West). + * + * @private + * @type String + * @default null + */ + var mouseResize = null; + + // shorthands / private variables + var sel = this.selection, + borderDouble = config.borderWidth * 2, + ev_canvasSizeChangeId = null, + ev_configChangeId = null, + ctrlKey = false, + shiftKey = false; + + /** + * The last selection rectangle that was drawn. This is used by the selection + * drawing functions. + * + * @private + * @type Object + */ + // We avoid retrieving the mouse coordinates during the mouseup event, due to + // the Opera bug DSK-232264. + var lastSel = null; + + /** + * The tool preactivation code. This function prepares the selection canvas + * element. + * + * @returns {Boolean} True if the activation did not fail, or false otherwise. + * If false is returned, the selection tool cannot be activated. + */ + this.preActivate = function () { + if (!('canvasContainer' in gui.elems)) { + alert(lang.errorToolActivate); + return false; + } + + // The selection image buffer. + sel.canvas = app.doc.createElement('canvas'); + if (!sel.canvas) { + alert(lang.errorToolActivate); + return false; + } + + sel.canvas.width = 5; + sel.canvas.height = 5; + + sel.context = sel.canvas.getContext('2d'); + if (!sel.context) { + alert(lang.errorToolActivate); + return false; + } + + sel.marquee = app.doc.createElement('div'); + if (!sel.marquee) { + alert(lang.errorToolActivate); + return false; + } + sel.marquee.className = gui.classPrefix + 'selectionMarquee'; + marqueeStyle = sel.marquee.style; + + return true; + }; + + /** + * The tool activation code. This method sets-up multiple event listeners for + * several target objects. + */ + this.activate = function () { + // Older browsers do not support get/putImageData, thus non-transparent + // selections cannot be used. + if (!layerContext.putImageData || !layerContext.getImageData) { + config.transparent = true; + } + + marqueeHide(); + + marqueeStyle.borderWidth = config.borderWidth + 'px'; + sel.marquee.addEventListener('mousedown', marqueeMousedown, false); + sel.marquee.addEventListener('mousemove', marqueeMousemove, false); + sel.marquee.addEventListener('mouseup', marqueeMouseup, false); + + gui.elems.canvasContainer.appendChild(sel.marquee); + + // Disable the Canvas shadow. + app.shadowDisallow(); + + // Application event listeners. + ev_canvasSizeChangeId = app.events.add('canvasSizeChange', + ev_canvasSizeChange); + ev_configChangeId = app.events.add('configChange', ev_configChange); + + // Register selection-related commands + app.commandRegister('selectionCrop', _self.selectionCrop); + app.commandRegister('selectionDelete', _self.selectionDelete); + app.commandRegister('selectionFill', _self.selectionFill); + + if (!timer) { + timer = setInterval(timerFn, app.config.toolDrawDelay); + } + + return true; + }; + + /** + * The tool deactivation code. This removes all event listeners and cleans up + * the document. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + _self.selectionMerge(); + + sel.marquee.removeEventListener('mousedown', marqueeMousedown, false); + sel.marquee.removeEventListener('mousemove', marqueeMousemove, false); + sel.marquee.removeEventListener('mouseup', marqueeMouseup, false); + + marqueeStyle = null; + gui.elems.canvasContainer.removeChild(sel.marquee); + + delete sel.context, sel.canvas, sel.marquee; + + // Re-enable canvas shadow. + app.shadowAllow(); + + // Remove the application event listeners. + if (ev_canvasSizeChangeId) { + app.events.remove('canvasSizeChange', ev_canvasSizeChangeId); + } + if (ev_configChangeId) { + app.events.remove('configChange', ev_configChangeId); + } + + // Unregister selection-related commands + app.commandUnregister('selectionCrop'); + app.commandUnregister('selectionDelete'); + app.commandUnregister('selectionFill'); + + return true; + }; + + /** + * The mousedown event handler. Depending on the mouse location, + * this method does initiate different selection operations: drawing, + * dropping, dragging or resizing. + * + *

Hold the Control key down to temporarily toggle the + * transformation mode. + * + * @param {Event} ev The DOM Event object. + */ + this.mousedown = function (ev) { + if (_self.state !== _self.STATE_NONE && + _self.state !== _self.STATE_SELECTED) { + return false; + } + + // Update the current mouse position, this is used as the start position for most of the operations. + x0 = mouse.x; + y0 = mouse.y; + + shiftKey = ev.shiftKey; + ctrlKey = ev.ctrlKey; + lastSel = null; + + // No selection is available, then start drawing a selection. + if (_self.state === _self.STATE_NONE) { + _self.state = _self.STATE_DRAWING; + marqueeStyle.display = ''; + gui.statusShow('selectionDraw'); + + return true; + } + + // STATE_SELECTED: selection available. + mouseAreaUpdate(); + + /* + * Check if the user clicked outside the selection: drop the selection, + * switch to STATE_PENDING, clear the image buffer and put the current + * selection buffer in the image layer. + * + * If the user moves the mouse without taking the finger off the mouse + * button, then a new selection rectangle will start to be drawn: the script + * will switch to STATE_DRAWING. + * + * If the user simply takes the finger off the mouse button (mouseup), then + * the script will switch to STATE_NONE (no selection available). + */ + switch (mouseArea) { + case 'out': + _self.state = _self.STATE_PENDING; + marqueeHide(); + gui.statusShow('selectionActive'); + selectionMergeStrict(); + + return true; + + case 'in': + // The mouse area: 'in' for drag. + _self.state = _self.STATE_DRAGGING; + gui.statusShow('selectionDrag'); + break; + + case 'border': + // 'border' for resize (the user is clicking on the borders). + _self.state = _self.STATE_RESIZING; + gui.statusShow('selectionResize'); + } + + // Temporarily toggle the transformation mode if the user holds the Control + // key down. + if (ev.ctrlKey) { + config.transform = !config.transform; + } + + // If there's any ImageData currently in memory, which was "cut" out from + // the current layer, then put it back on the layer. This needs to be done + // only when the selection.transform mode is not active - that's when the + // drag/resize operation only changes the selection, not the pixels + // themselves. + if (sel.layerCleared && !config.transform) { + selectionMergeStrict(); + + } else if (!sel.layerCleared && config.transform) { + // When the user starts dragging/resizing the ImageData we must cut out + // the current selection from the image layer. + selectionBufferInit(); + } + + return true; + }; + + /** + * The mousemove event handler. + * + * @param {Event} ev The DOM Event object. + */ + this.mousemove = function (ev) { + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * The timer function. When the mouse button is down, this method performs the + * dragging/resizing operation. When the mouse button is not down, this method + * simply tracks the mouse location for the purpose of determining the area + * being pointed at: the selection, the borders, or if the mouse is outside + * the selection. + * @private + */ + function timerFn () { + if (!needsRedraw) { + return; + } + + switch (_self.state) { + case _self.STATE_PENDING: + // selection dropped, switch to draw selection + _self.state = _self.STATE_DRAWING; + marqueeStyle.display = ''; + gui.statusShow('selectionDraw'); + + case _self.STATE_DRAWING: + selectionDraw(); + break; + + case _self.STATE_SELECTED: + mouseAreaUpdate(); + break; + + case _self.STATE_DRAGGING: + selectionDrag(); + break; + + case _self.STATE_RESIZING: + selectionResize(); + } + + needsRedraw = false; + }; + + /** + * The mouseup event handler. This method ends any selection + * operation. + * + *

This method dispatches the {@link pwlib.appEvent.selectionChange} + * application event when the selection state is changed or when the selection + * size/location is updated. + * + * @param {Event} ev The DOM Event object. + */ + this.mouseup = function (ev) { + // Allow click+mousemove+click, not only mousedown+move+up + if (_self.state !== _self.STATE_PENDING && + mouse.x === x0 && mouse.y === y0) { + return true; + } + + needsRedraw = false; + + shiftKey = ev.shiftKey; + if (ctrlKey) { + config.transform = !config.transform; + } + + if (_self.state === _self.STATE_PENDING) { + // Selection dropped? If yes, switch to the no selection state. + _self.state = _self.STATE_NONE; + app.events.dispatch(new appEvent.selectionChange(_self.state)); + + return true; + + } else if (!lastSel) { + _self.state = _self.STATE_NONE; + marqueeHide(); + gui.statusShow('selectionActive'); + app.events.dispatch(new appEvent.selectionChange(_self.state)); + + return true; + } + + sel.x = lastSel.x; + sel.y = lastSel.y; + + if ('width' in lastSel) { + sel.width = lastSel.width; + sel.height = lastSel.height; + } + + _self.state = _self.STATE_SELECTED; + + app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, + sel.width, sel.height)); + + gui.statusShow('selectionAvailable'); + + return true; + }; + + /** + * The mousedown event handler for the selection marquee element. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function marqueeMousedown (ev) { + if (mouse.buttonDown) { + return; + } + mouse.buttonDown = true; + + ev.preventDefault(); + + _self.mousedown(ev); + }; + + /** + * The mousemove event handler for the selection marquee element. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function marqueeMousemove (ev) { + if ('layerX' in ev) { + mouse.x = MathRound((this.offsetLeft + ev.layerX) / image.canvasScale); + mouse.y = MathRound((this.offsetTop + ev.layerY) / image.canvasScale); + } else if ('offsetX' in ev) { + mouse.x = MathRound((this.offsetLeft + ev.offsetX) / image.canvasScale); + mouse.y = MathRound((this.offsetTop + ev.offsetY) / image.canvasScale); + } + + shiftKey = ev.shiftKey; + needsRedraw = true; + }; + + /** + * The mouseup event handler for the selection marquee element. + * + * @private + * @param {Event} ev The DOM Event object. + */ + function marqueeMouseup (ev) { + if (!mouse.buttonDown) { + return; + } + mouse.buttonDown = false; + + ev.preventDefault(); + + _self.mouseup(ev); + }; + + /** + * Hide the selection marquee element. + * @private + */ + function marqueeHide () { + marqueeStyle.display = 'none'; + marqueeStyle.top = '-' + (borderDouble + 50) + 'px'; + marqueeStyle.left = '-' + (borderDouble + 50) + 'px'; + marqueeStyle.width = '1px'; + marqueeStyle.height = '1px'; + marqueeStyle.cursor = ''; + }; + + /** + * Perform the selection rectangle drawing operation. + * + * @private + */ + function selectionDraw () { + var x = MathMin(mouse.x, x0), + y = MathMin(mouse.y, y0), + w = MathAbs(mouse.x - x0), + h = MathAbs(mouse.y - y0); + + // Constrain the shape to a square. + if (shiftKey) { + if (w > h) { + if (y === mouse.y) { + y -= w-h; + } + h = w; + } else { + if (x === mouse.x) { + x -= h-w; + } + w = h; + } + } + + var mw = w * image.canvasScale - borderDouble, + mh = h * image.canvasScale - borderDouble; + + if (mw < 1 || mh < 1) { + lastSel = null; + return; + } + + marqueeStyle.top = (y * image.canvasScale) + 'px'; + marqueeStyle.left = (x * image.canvasScale) + 'px'; + marqueeStyle.width = mw + 'px'; + marqueeStyle.height = mh + 'px'; + + lastSel = {'x': x, 'y': y, 'width': w, 'height': h}; + }; + + /** + * Perform the selection drag operation. + * + * @private + * + * @returns {false|Array} False is returned if the selection is too small, + * otherwise an array of two elements is returned. The array holds the + * selection coordinates, x and y. + */ + function selectionDrag () { + // Snapping on the X/Y axis + if (shiftKey) { + snapXY(x0, y0); + } + + var x = sel.x + mouse.x - x0, + y = sel.y + mouse.y - y0; + + // Dragging the ImageData + if (config.transform) { + bufferContext.clearRect(0, 0, image.width, image.height); + + if (!config.transparent) { + bufferContext.fillRect(x, y, sel.width, sel.height); + } + + // Parameters: + // source image, dest x, dest y, dest width, dest height + bufferContext.drawImage(sel.canvas, x, y, sel.width, sel.height); + } + + marqueeStyle.top = (y * image.canvasScale) + 'px'; + marqueeStyle.left = (x * image.canvasScale) + 'px'; + + lastSel = {'x': x, 'y': y}; + }; + + /** + * Perform the selection resize operation. + * + * @private + * + * @returns {false|Array} False is returned if the selection is too small, + * otherwise an array of four elements is returned. The array holds the + * selection information: x, y, width and height. + */ + function selectionResize () { + var diffx = mouse.x - x0, + diffy = mouse.y - y0, + x = sel.x, + y = sel.y, + w = sel.width, + h = sel.height; + + switch (mouseResize) { + case 'nw': + x += diffx; + y += diffy; + w -= diffx; + h -= diffy; + break; + case 'n': + y += diffy; + h -= diffy; + break; + case 'ne': + y += diffy; + w += diffx; + h -= diffy; + break; + case 'e': + w += diffx; + break; + case 'se': + w += diffx; + h += diffy; + break; + case 's': + h += diffy; + break; + case 'sw': + x += diffx; + w -= diffx; + h += diffy; + break; + case 'w': + x += diffx; + w -= diffx; + break; + default: + lastSel = null; + return; + } + + if (!w || !h) { + lastSel = null; + return; + } + + // Constrain the rectangle to have the same aspect ratio as the initial + // rectangle. + if (shiftKey) { + var p = sel.width / sel.height, + w2 = w, + h2 = h; + + switch (mouseResize.charAt(0)) { + case 'n': + case 's': + w2 = MathRound(h*p); + break; + default: + h2 = MathRound(w/p); + } + + switch (mouseResize) { + case 'nw': + case 'sw': + x -= w2 - w; + y -= h2 - h; + } + + w = w2; + h = h2; + } + + if (w < 0) { + x += w; + w *= -1; + } + if (h < 0) { + y += h; + h *= -1; + } + + var mw = w * image.canvasScale - borderDouble, + mh = h * image.canvasScale - borderDouble; + + if (mw < 1 || mh < 1) { + lastSel = null; + return; + } + + // Resizing the ImageData + if (config.transform) { + bufferContext.clearRect(0, 0, image.width, image.height); + + if (!config.transparent) { + bufferContext.fillRect(x, y, w, h); + } + + // Parameters: + // source image, dest x, dest y, dest width, dest height + bufferContext.drawImage(sel.canvas, x, y, w, h); + } + + marqueeStyle.top = (y * image.canvasScale) + 'px'; + marqueeStyle.left = (x * image.canvasScale) + 'px'; + marqueeStyle.width = mw + 'px'; + marqueeStyle.height = mh + 'px'; + + lastSel = {'x': x, 'y': y, 'width': w, 'height': h}; + }; + + /** + * Determine the are where the mouse is located: if it is inside or outside of + * the selection rectangle, or on the selection border. + * @private + */ + function mouseAreaUpdate () { + var border = config.borderWidth / image.canvasScale, + cursor = '', + x1_out = sel.x + sel.width, + y1_out = sel.y + sel.height, + x1_in = x1_out - border, + y1_in = y1_out - border, + x0_out = sel.x, + y0_out = sel.y, + x0_in = sel.x + border, + y0_in = sel.y + border; + + mouseArea = 'out'; + + // Inside the rectangle + if (mouse.x < x1_in && mouse.y < y1_in && + mouse.x > x0_in && mouse.y > y0_in) { + cursor = 'move'; + mouseArea = 'in'; + + } else { + // On one of the borders (north/south) + if (mouse.x >= x0_out && mouse.x <= x1_out && + mouse.y >= y0_out && mouse.y <= y0_in) { + cursor = 'n'; + + } else if (mouse.x >= x0_out && mouse.x <= x1_out && + mouse.y >= y1_in && mouse.y <= y1_out) { + cursor = 's'; + } + + // West/east + if (mouse.y >= y0_out && mouse.y <= y1_out && + mouse.x >= x0_out && mouse.x <= x0_in) { + cursor += 'w'; + + } else if (mouse.y >= y0_out && mouse.y <= y1_out && + mouse.x >= x1_in && mouse.x <= x1_out) { + cursor += 'e'; + } + + if (cursor !== '') { + mouseResize = cursor; + cursor += '-resize'; + mouseArea = 'border'; + } + } + + // Due to bug 126457 Opera will not automatically update the cursor, + // therefore they will not see any visual feedback. + if (cursor !== marqueeStyle.cursor) { + marqueeStyle.cursor = cursor; + } + }; + + /** + * The canvasSizeChange application event handler. This method + * makes sure the selection size stays in sync. + * + * @private + * @param {pwlib.appEvent.canvasSizeChange} ev The application event object. + */ + function ev_canvasSizeChange (ev) { + if (_self.state !== _self.STATE_SELECTED) { + return; + } + + marqueeStyle.top = (sel.y * ev.scale) + 'px'; + marqueeStyle.left = (sel.x * ev.scale) + 'px'; + marqueeStyle.width = (sel.width * ev.scale - borderDouble) + 'px'; + marqueeStyle.height = (sel.height * ev.scale - borderDouble) + 'px'; + }; + + /** + * The configChange application event handler. This method makes + * sure that changes to the selection transparency configuration option are + * applied. + * + * @private + * @param {pwlib.appEvent.configChange} ev The application event object. + */ + function ev_configChange (ev) { + // Continue only if the selection rectangle is available. + if (ev.group !== 'selection' || ev.config !== 'transparent' || + !config.transform || _self.state !== _self.STATE_SELECTED) { + return; + } + + if (!sel.layerCleared) { + selectionBufferInit(); + } + + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + + if (!ev.value) { + bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height); + } + + // Draw the updated selection + bufferContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height); + }; + + /** + * Initialize the selection buffer, when the user starts dragging or resizing + * the selected pixels. + * + * @private + */ + function selectionBufferInit () { + var x = sel.x, + y = sel.y, + w = sel.width, + h = sel.height, + sumX = sel.x + sel.width, + sumY = sel.y + sel.height, + dx = 0, dy = 0; + + sel.widthOriginal = w; + sel.heightOriginal = h; + + if (x < 0) { + w += x; + dx -= x; + x = 0; + } + if (y < 0) { + h += y; + dy -= y; + y = 0; + } + + if (sumX > image.width) { + w = image.width - sel.x; + } + if (sumY > image.height) { + h = image.height - sel.y; + } + + if (!config.transparent) { + bufferContext.fillRect(x, y, w, h); + } + + // Parameters: + // source image, src x, src y, src w, src h, dest x, dest y, dest w, dest h + bufferContext.drawImage(layerCanvas, x, y, w, h, x, y, w, h); + + sel.canvas.width = sel.widthOriginal; + sel.canvas.height = sel.heightOriginal; + + // Also put the selected pixels into the selection buffer. + sel.context.drawImage(layerCanvas, x, y, w, h, dx, dy, w, h); + + // Clear the selected pixels from the image + layerContext.clearRect(x, y, w, h); + sel.layerCleared = true; + + app.historyAdd(); + }; + + /** + * Perform the selection buffer merge onto the current image layer. + * @private + */ + function selectionMergeStrict () { + if (!sel.layerCleared) { + return; + } + + if (!config.transparent) { + layerContext.fillRect(sel.x, sel.y, sel.width, sel.height); + } + + layerContext.drawImage(sel.canvas, sel.x, sel.y, sel.width, sel.height); + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + + sel.layerCleared = false; + sel.canvas.width = 5; + sel.canvas.height = 5; + + app.historyAdd(); + }; + + /** + * Merge the selection buffer onto the current image layer. + * + *

This method dispatches the {@link pwlib.appEvent.selectionChange} + * application event. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionMerge = function () { + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + selectionMergeStrict(); + + _self.state = _self.STATE_NONE; + marqueeHide(); + gui.statusShow('selectionActive'); + + app.events.dispatch(new appEvent.selectionChange(_self.state)); + + return true; + }; + + /** + * Select all the entire image. + * + *

This method dispatches the {@link pwlib.appEvent.selectionChange} + * application event. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectAll = function () { + if (_self.state !== _self.STATE_NONE && _self.state !== + _self.STATE_SELECTED) { + return false; + } + + if (_self.state === _self.STATE_SELECTED) { + selectionMergeStrict(); + } else { + _self.state = _self.STATE_SELECTED; + marqueeStyle.display = ''; + } + + sel.x = 0; + sel.y = 0; + sel.width = image.width; + sel.height = image.height; + + marqueeStyle.top = '0px'; + marqueeStyle.left = '0px'; + marqueeStyle.width = (sel.width*image.canvasScale - borderDouble) + 'px'; + marqueeStyle.height = (sel.height*image.canvasScale - borderDouble) + 'px'; + + mouseAreaUpdate(); + + app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, + sel.width, sel.height)); + + return true; + }; + + /** + * Cut the selected pixels. The associated ImageData is stored in {@link + * PaintWeb#clipboard}. + * + *

This method dispatches two application events: {@link + * pwlib.appEvent.clipboardUpdate} and {@link pwlib.appEvent.selectionChange}. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionCut = function () { + if (!_self.selectionCopy()) { + return false; + } + + if (sel.layerCleared) { + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + + sel.canvas.width = 5; + sel.canvas.height = 5; + sel.layerCleared = false; + + } else { + layerContext.clearRect(sel.x, sel.y, sel.width, sel.height); + app.historyAdd(); + } + + _self.state = _self.STATE_NONE; + marqueeHide(); + + app.events.dispatch(new appEvent.selectionChange(_self.state)); + gui.statusShow('selectionActive'); + + return true; + }; + + /** + * Copy the selected pixels. The associated ImageData is stored in {@link + * PaintWeb#clipboard}. + * + *

This method dispatches the {@link pwlib.appEvent.clipboardUpdate} + * application event. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionCopy = function () { + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + if (!layerContext.getImageData || !layerContext.putImageData) { + alert(lang.errorClipboardUnsupported); + return false; + } + + if (!sel.layerCleared) { + var w = sel.width, + h = sel.height, + sumX = sel.width + sel.x; + sumY = sel.height + sel.y; + + if (sumX > image.width) { + w = image.width - sel.x; + } + if (sumY > image.height) { + h = image.height - sel.y; + } + + try { + app.clipboard = layerContext.getImageData(sel.x, sel.y, w, h); + } catch (err) { + alert(lang.failedSelectionCopy); + return false; + } + + } else { + try { + app.clipboard = sel.context.getImageData(0, 0, sel.widthOriginal, + sel.heightOriginal); + } catch (err) { + alert(lang.failedSelectionCopy); + return false; + } + } + + app.events.dispatch(new appEvent.clipboardUpdate(app.clipboard)); + + return true; + }; + + /** + * Paste an image from the "clipboard". The {@link PaintWeb#clipboard} object + * must be an ImageData. This method will generate a new selection which will + * hold the pasted image. + * + *

The {@link pwlib.appEvent.selectionChange} application event is + * dispatched. + * + *

If the {@link PaintWeb.config.selection.transform} value is false, then + * it becomes true. The {@link pwlib.appEvent.configChange} application is + * then dispatched. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.clipboardPaste = function () { + if (!app.clipboard || _self.state !== _self.STATE_NONE && _self.state !== + _self.STATE_SELECTED) { + return false; + } + + if (!layerContext.getImageData || !layerContext.putImageData) { + alert(lang.errorClipboardUnsupported); + return false; + } + + // The default position for the pasted image is the top left corner of the + // visible area, taking into consideration the zoom level. + var x = MathRound(gui.elems.viewport.scrollLeft / image.canvasScale), + y = MathRound(gui.elems.viewport.scrollTop / image.canvasScale), + w = app.clipboard.width, + h = app.clipboard.height; + + sel.canvas.width = w; + sel.canvas.height = h; + sel.context.putImageData(app.clipboard, 0, 0); + + if (_self.state === _self.STATE_SELECTED) { + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + } else { + _self.state = _self.STATE_SELECTED; + } + + if (!config.transparent) { + bufferContext.fillRect(x, y, w, h); + } + bufferContext.drawImage(sel.canvas, x, y, w, h); + + sel.widthOriginal = sel.width = w; + sel.heightOriginal = sel.height = h; + sel.x = x; + sel.y = y; + sel.layerCleared = true; + + marqueeStyle.top = (y * image.canvasScale) + 'px'; + marqueeStyle.left = (x * image.canvasScale) + 'px'; + marqueeStyle.width = (w * image.canvasScale - borderDouble) + 'px'; + marqueeStyle.height = (h * image.canvasScale - borderDouble) + 'px'; + marqueeStyle.display = ''; + + if (!config.transform) { + config.transform = true; + app.events.dispatch(new appEvent.configChange(true, false, 'transform', + 'selection', config)); + } + + mouseAreaUpdate(); + + app.events.dispatch(new appEvent.selectionChange(_self.state, sel.x, sel.y, + sel.width, sel.height)); + + gui.statusShow('selectionAvailable'); + + return true; + }; + + /** + * Perform selection delete. + * + *

This method changes the {@link PaintWeb.config.selection.transform} + * value to false if the current selection has pixels that are currently being + * manipulated. In such cases, the {@link pwlib.appEvent.configChange} + * application event is also dispatched. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionDelete = function () { + // Delete the pixels from the image if they are not deleted already. + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + if (!sel.layerCleared) { + layerContext.clearRect(sel.x, sel.y, sel.width, sel.height); + app.historyAdd(); + + } else { + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + sel.layerCleared = false; + sel.canvas.width = 5; + sel.canvas.height = 5; + + if (config.transform) { + config.transform = false; + app.events.dispatch(new appEvent.configChange(false, true, 'transform', + 'selection', config)); + } + } + + return true; + }; + + /** + * Drop the current selection. + * + *

This method dispatches the {@link pwlib.appEvent.selectionChange} + * application event. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionDrop = function () { + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + if (sel.layerCleared) { + bufferContext.clearRect(sel.x, sel.y, sel.width, sel.height); + sel.canvas.width = 5; + sel.canvas.height = 5; + sel.layerCleared = false; + } + + _self.state = _self.STATE_NONE; + + marqueeHide(); + gui.statusShow('selectionActive'); + + app.events.dispatch(new appEvent.selectionChange(_self.state)); + + return true; + }; + + /** + * Fill the available selection with the current + * bufferContext.fillStyle. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionFill = function () { + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + if (sel.layerCleared) { + sel.context.fillStyle = bufferContext.fillStyle; + sel.context.fillRect(0, 0, sel.widthOriginal, sel.heightOriginal); + bufferContext.fillRect(sel.x, sel.y, sel.width, sel.height); + + } else { + layerContext.fillStyle = bufferContext.fillStyle; + layerContext.fillRect(sel.x, sel.y, sel.width, sel.height); + app.historyAdd(); + } + + return true; + }; + + /** + * Crop the image to selection width and height. The selected pixels become + * the image itself. + * + *

This method invokes the {@link this#selectionMerge} and {@link + * PaintWeb#imageCrop} methods. + * + * @returns {Boolean} True if the operation was successful, or false if not. + */ + this.selectionCrop = function () { + if (_self.state !== _self.STATE_SELECTED) { + return false; + } + + _self.selectionMerge(); + + var w = sel.width, + h = sel.height, + sumX = sel.x + w, + sumY = sel.y + h; + + if (sumX > image.width) { + w -= sumX - image.width; + } + if (sumY > image.height) { + h -= sumY - image.height; + } + + app.imageCrop(sel.x, sel.y, w, h); + + return true; + }; + + /** + * The keydown event handler. This method calls selection-related + * commands associated to keyboard shortcuts. + * + * @param {Event} ev The DOM Event object. + * + * @returns {Boolean} True if the keyboard shortcut was recognized, or false + * if not. + * + * @see PaintWeb.config.selection.keys holds the keyboard shortcuts + * configuration. + */ + this.keydown = function (ev) { + switch (ev.kid_) { + case config.keys.transformToggle: + // Toggle the selection transformation mode. + config.transform = !config.transform; + app.events.dispatch(new appEvent.configChange(config.transform, + !config.transform, 'transform', 'selection', config)); + break; + + case config.keys.selectionCrop: + return _self.selectionCrop(ev); + + case config.keys.selectionDelete: + return _self.selectionDelete(ev); + + case config.keys.selectionDrop: + return _self.selectionDrop(ev); + + case config.keys.selectionFill: + return _self.selectionFill(ev); + + default: + return false; + } + + return true; + }; +}; + +/** + * @class Selection change event. This event is not cancelable. + * + * @augments pwlib.appEvent + * + * @param {Number} state Tells the new state of the selection. + * @param {Number} [x] Selection start position on the x-axis of the image. + * @param {Number} [y] Selection start position on the y-axis of the image. + * @param {Number} [width] Selection width. + * @param {Number} [height] Selection height. + */ +pwlib.appEvent.selectionChange = function (state, x, y, width, height) { + /** + * No selection is available. + * @constant + */ + this.STATE_NONE = 0; + + /** + * Selection available. + * @constant + */ + this.STATE_SELECTED = 2; + + /** + * Selection state. + * @type Number + */ + this.state = state; + + /** + * Selection location on the x-axis of the image. + * @type Number + */ + this.x = x; + + /** + * Selection location on the y-axis of the image. + * @type Number + */ + this.y = y; + + /** + * Selection width. + * @type Number + */ + this.width = width; + + /** + * Selection height. + * @type Number + */ + this.height = height; + + pwlib.appEvent.call(this, 'selectionChange'); +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/biz/paintweb/tools/text.js b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/text.js new file mode 100644 index 00000000..0d255db3 --- /dev/null +++ b/src/main/webapp/resources/lib/fims/biz/paintweb/tools/text.js @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2018 gujc + * + * This file is part of PaintWeb and modified by gujc. + * + */ + +/** + * @class The text tool. + * + * @param {PaintWeb} app Reference to the main paint application object. + */ +pwlib.tools.text = function (app) { + var _self = this, + clearInterval = app.win.clearInterval, + config = app.config.text, + context = app.buffer.context, + doc = app.doc, + gui = app.gui, + image = app.image, + lang = app.lang, + MathRound = Math.round, + mouse = app.mouse, + setInterval = app.win.setInterval; + + /** + * The interval ID used for invoking the drawing operation every few + * milliseconds. + * + * @private + * @see PaintWeb.config.toolDrawDelay + */ + var timer = null; + + /** + * Holds the previous tool ID. + * + * @private + * @type String + */ + var prevTool = app.tool ? app.tool._id : null; + + /** + * Tells if the drawing canvas needs to be updated or not. + * + * @private + * @type Boolean + * @default false + */ + var inputText = null, + input_fontFamily = null, + ev_configChangeId = null, + beforeMouse = null; + var fontSizeArr = ["8", "10", "12", "14", "18", "24"]; + + /** + * Tool preactivation code. This method check if the browser has support for + * rendering text in Canvas. + * + * @returns {Boolean} True if the tool can be activated successfully, or false + * if not. + */ + /*this.preActivate = function () { + if (!gui.inputs.textString || !gui.inputs.text_fontFamily || + !gui.elems.viewport) { + return false; + + } + + // Canvas 2D Text API + if (context.fillText && context.strokeText) { + return true; + } +*/ + // Opera can only render text via SVG Text. + // Note: support for Opera has been disabled. + // There are severe SVG redraw issues when updating the SVG text element. + // Besides, there are important memory leaks. + // Ultimately, there's a deal breaker: security violation. The SVG document + // which is rendered inside Canvas is considered "external" + // - get/putImageData() and toDataURL() stop working after drawImage(svg) is + // invoked. Eh. + /*if (pwlib.browser.opera) { + return true; + }*/ + + // Gecko 1.9.0 had its own proprietary Canvas 2D Text API. +/* if (context.mozPathText) { + return true; + } + + alert(lang.errorTextUnsupported); + return false; + };*/ + + /** + * The tool activation code. This sets up a few variables, starts the drawing + * timer and adds event listeners as needed. + */ + this.activate = function () { + // Reset the mouse coordinates to the scroll top/left corner such that the + // text is rendered there. + mouse.x = Math.round(gui.elems.viewport.scrollLeft / image.canvasScale), + mouse.y = Math.round(gui.elems.viewport.scrollTop / image.canvasScale), + + app.shadowDisallow(); + + input_fontFamily = gui.inputs.text_fontFamily; + if (!inputText) { + inputText = doc.createElement('div'); + inputText.className = 'inputText'; + inputText.contentEditable = true; + inputText.addEventListener('keydown', this.keydown, false); + gui.elems.canvasContainer.appendChild(inputText); + } + + ev_configChangeId = app.events.add('configChange', ev_configChange); + }; + + function initInputText() { + inputText.innerHTML = '

 

'; + inputText.firstChild.style.fontFamily = input_fontFamily.value; + inputText.firstChild.style.fontSize = fontSizeArr[gui.inputs.fontSize.selectedIndex] + "pt"; + inputText.firstChild.style.color = gui.app.buffer.context.strokeStyle;//gui.colorInputs.strokeStyle.color + setSelection(inputText.firstChild.firstChild); + } + /** + * The tool deactivation simply consists of removing the event listeners added + * when the tool was constructed, and clearing the buffer canvas. + */ + this.deactivate = function () { + if (timer) { + clearInterval(timer); + timer = null; + } + + app.shadowAllow(); + drawText("none"); + + if (ev_configChangeId) { + app.events.remove('configChange', ev_configChangeId); + } + + context.clearRect(0, 0, image.width, image.height); + + return true; + }; + + /** + * The configChange application event handler. This is also the + * input and change event handler for the text + * string input element. This method updates the Canvas text-related + * properties as needed, and re-renders the text. + * + *

This function is not used on Opera. + * + * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event + * object. + */ + function ev_configChange (ev) { + if (ev.type === 'input' || ev.type === 'change' || + (!ev.group && ev.config === 'shapeType') || + (ev.group === 'line' && ev.config === 'lineWidth')) { + + return; + } + + if (ev.type !== 'configChange' && ev.group !== 'text') { + return; + } + + var font = ''; + + switch (ev.config) { + case 'fontFamily': + if (ev.value === '+') { + fontFamilyAdd(ev); + } else { + document.execCommand("fontName", false, ev.value); + } + break; + case 'fontSize': document.execCommand("fontSize", false, ev.value); break; + case 'bold': + case 'italic': document.execCommand(ev.config, false, ''); break; + case 'left': document.execCommand('justifyLeft', false, null); break; + case 'right': document.execCommand('justifyRight', false, null); break; + case 'center': document.execCommand('justifyCenter', false, null); break; + //case 'fillStyle': + case 'strokeStyle': document.execCommand('foreColor', false, rgbToHex(ev.value)); break; + } + + }; + + function rgbToHex (rgb) { + rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + return (rgb && rgb.length === 4) ? "#" + + ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : ''; + } + /** + * Add a new font family into the font family drop down. This function is + * invoked by the ev_configChange() function when the user + * attempts to add a new font family. + * + * + * @private + * + * @param {pwlib.appEvent.configChange} ev The application event object. + */ + function fontFamilyAdd (ev) { + var new_font = prompt(lang.promptTextFont) || ''; + new_font = new_font.replace(/^\s+/, '').replace(/\s+$/, '') || + ev.previousValue; + + // Check if the font name is already in the list. + var opt, new_font2 = new_font.toLowerCase(), + n = input_fontFamily.options.length; + + for (var i = 0; i < n; i++) { + opt = input_fontFamily.options[i]; + if (opt.value.toLowerCase() == new_font2) { + config.fontFamily = opt.value; + input_fontFamily.selectedIndex = i; + input_fontFamily.value = config.fontFamily; + ev.value = config.fontFamily; + + return; + } + } + + // Add the new font. + opt = doc.createElement('option'); + opt.value = new_font; + opt.appendChild(doc.createTextNode(new_font)); + input_fontFamily.insertBefore(opt, input_fontFamily.options[n-1]); + input_fontFamily.selectedIndex = n-1; + input_fontFamily.value = new_font; + ev.value = new_font; + config.fontFamily = new_font; + }; + + /** + * The click event handler. This method completes the drawing + * operation by inserting the text into the layer canvas. + */ + this.click = function () { + if (beforeMouse) { + drawText (null); + } else{ // first click + beforeMouse = {x: mouse.x, y: mouse.y}; + initInputText(); + } + var zoom = gui.app.image.zoom; + inputText.style.display = "block"; + inputText.style.top = (mouse.y*zoom) + "px"; + inputText.style.left = (mouse.x*zoom) + "px"; + inputText.focus(); + }; + + function getSelection() { + return (window.getSelection) ? window.getSelection() : document.selection; + } + function getRange() { + var sel, range; + if (window.getSelection) { + sel = window.getSelection(); + if (sel.getRangeAt && sel.rangeCount) { + range = sel.getRangeAt(0); + return range; + } + } else if (document.selection && document.selection.type !== "Control") { + return document.selection.createRange(); + } + } + + function setSelection(node ) { + var range = getRange(); + if (!range) {return;} + if (window.getSelection) { + range.setStartBefore(node); + range.setEndAfter(node); + var sel = getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } else { + range.select(); + } + } + + function drawText (display) { + if (inputText.innerText === "") { + if (display) inputText.style.display = display;//"none"; + beforeMouse = {x: mouse.x, y: mouse.y}; + return; + } + + inputText.style.border = "0px"; + html2canvas(inputText, {"backgroundColor": "transparent"}).then(function(canvas) { + inputText.style.border = ""; + initInputText(); + + var zoom = gui.app.image.zoom; + context.drawImage(canvas, (beforeMouse.x*zoom), (beforeMouse.y*zoom)); + app.layerUpdate(); + + if (display) inputText.style.display = display;//"none"; + beforeMouse = {x: mouse.x, y: mouse.y}; + }); + } + + /** + * The keydown event handler allows users to press the + * Escape key to cancel the drawing operation and return to the + * previous tool. + * + * @param {Event} ev The DOM Event object. + * @returns {Boolean} True if the key was recognized, or false if not. + */ + this.keydown = function (ev) { + //if (!prevTool || ev.kid_ != 'Escape') { + if (!prevTool || ev.keyCode != 27) { // esc + return false; + } + + inputText.innerText = ""; + + mouse.buttonDown = false; + app.toolActivate(prevTool, ev); + + return true; + }; +}; + +// vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: + diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.css b/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.css new file mode 100644 index 00000000..e26ef64f --- /dev/null +++ b/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.css @@ -0,0 +1,46 @@ +/* 해당 #3f51b5 색상코드 변경 시 디자인 일괄적용 */ + + +/* datepicker Style */ +.ui-datepicker { width: 270px; padding: 0; display: none; border: 0; margin-top: 10px;z-index: 15 !important;} +.ui-widget.ui-widget-content { border: 0; border-radius: 3px; overflow: hidden; background: #fff; box-shadow: 0 0 8px rgba(0,0,0,0.3) } +.ui-datepicker .ui-widget-header { position: relative; padding: 6px 0 36px 0; border: 0; background: #3f51b5; color: #fff; border-radius: 0; } +.ui-datepicker select.ui-datepicker-month, .ui-datepicker select.ui-datepicker-year { width: 70px; height: 26px; line-height: 26px; background: rgba(0,0,0,0.15) url(images/arrow-button.png) 100% -27px no-repeat; margin: 0 4px; padding: 0 6px; border: 0; border-radius: 3px; color: #fff; font-size: 13px; font-weight: bold; box-sizing: border-box; -webkit-appearance: none; -moz-appearance: none; appearance: none; /* 화살표 없애기 */ } +.ui-datepicker select.ui-datepicker-month:hover, .ui-datepicker select.ui-datepicker-year:hover { background: rgba(0,0,0,0.3) url(images/arrow-button.png) 100% -27px no-repeat; } +.ui-datepicker select::-ms-expand { +display: none; /* 화살표 없애기 for IE10, 11*/ +} +.ui-datepicker .ui-datepicker-title { line-height: 1.8em; text-align: center; margin: 0px 2.3em; font-size:13px; font-weight:bold; letter-spacing:0; } +.ui-datepicker-title .ui-datepicker-year option, .ui-datepicker-title .ui-datepicker-month option { background: #fff; color: #333; } +.ui-datepicker-title .ui-datepicker-year { } +.ui-datepicker-title .ui-datepicker-month { } +.ui-datepicker .ui-datepicker-calendar { position: relative; width: 260px; padding-top: 100px; margin: 0 auto; } +.ui-datepicker-calendar thead tr { position: absolute; top: -24px; left: 5px; right: 5px; } +.ui-datepicker-calendar thead th { float: left; display: block; padding: 0; width: 14.2857%; color: #fff; font-size:11px; letter-spacing:1px; opacity: 0.6 } +.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position: absolute; top: 5px; width: 1.8em; height: 1.8em; text-indent: -9999px; cursor: pointer } +.ui-datepicker .ui-datepicker-prev { left: 5px; } +.ui-datepicker .ui-datepicker-prev.ui-state-hover { background: rgba(0,0,0,0.1); border: 0; } +.ui-datepicker .ui-datepicker-prev .ui-icon { background: url(images/arrow-button.png) 0 0 no-repeat; } +.ui-datepicker .ui-datepicker-next { right: 5px; } +.ui-datepicker .ui-datepicker-next.ui-state-hover { background: rgba(0,0,0,0.1); border: 0; } +.ui-datepicker .ui-datepicker-next .ui-icon { background: url(images/arrow-button.png) -16px 0 no-repeat; } +.ui-datepicker .ui-state-default { display: block; border: 0; border-radius: 15px; width: 30px; height: 30px; line-height: 30px; padding: 0; margin: 4px auto; font-size: 12px; text-align: center; background: #fff; font-weight: normal; color: #333; box-sizing: border-box; } +.ui-datepicker .ui-state-default.ui-state-hover { background: rgba(0,0,0,0.05); } +.ui-datepicker-today .ui-state-default { color: #3f51b5; font-weight: bold; } +.ui-datepicker-current-day .ui-state-default { background: #3f51b5 !important; color: #fff; font-weight: bold; } +.ui-datepicker-buttonpane { overflow: hidden; border-top: 1px solid #eee; } +.ui-datepicker-buttonpane button[data-handler="today"] { color: #3f51b5; opacity: 0.75; } +.ui-datepicker-buttonpane button[data-handler="hide"] { } + +/* ie add Style */ +.ui-datepicker td, .ui-datepicker th { border: 0 } +.ui-datepicker .ui-datepicker-buttonpane button { float: right; cursor: pointer; width: auto; margin: 0.5em 0.2em 0.4em; padding: 0.2em 0.6em 0.3em; overflow: visible; } +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float: left; } +.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } +.ui-corner-all { border-radius: 3px; } +.ui-icon { display: block; width: 16px; height: 16px; } + +/* mobile Responsive */ + @media screen and (max-width:480px) { +.ui-datepicker { width: 270px !important; left: 50% !important; margin-left: -135px !important; } +} diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.js b/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.js new file mode 100644 index 00000000..dc050aae --- /dev/null +++ b/src/main/webapp/resources/lib/fims/framework/datepicker/datepicker.js @@ -0,0 +1,23 @@ +/** + * datepicker 설정 + */ +$.datepicker.regional["ko"] = { + dateFormat: 'yy-mm-dd', + prevText: '이전달', + nextText: '다음달', + currentText: '오늘', + closeText: '닫기', + monthNames: ["1월(JAN)","2월(FEB)","3월(MAR)","4월(APR)","5월(MAY)","6월(JUN)", "7월(JUL)","8월(AUG)","9월(SEP)","10월(OCT)","11월(NOV)","12월(DEC)"], + monthNamesShort: ["1월","2월","3월","4월","5월","6월", "7월","8월","9월","10월","11월","12월"], + dayNames: ["일","월","화","수","목","금","토"], + dayNamesShort: ["일","월","화","수","목","금","토"], + dayNamesMin: ["일","월","화","수","목","금","토"], + changeMonth: true, // month 셀렉트박스 사용 + changeYear: true, // year 셀렉트박스 사용 + weekHeader: "Wk", + firstDay: 0, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: "" +}; +$.datepicker.setDefaults($.datepicker.regional["ko"]); diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/arrow-button.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/arrow-button.png new file mode 100644 index 00000000..eddaaf27 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/arrow-button.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_444444_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_444444_256x240.png new file mode 100644 index 00000000..19f664d9 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_444444_256x240.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_555555_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_555555_256x240.png new file mode 100644 index 00000000..e965f6d9 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_555555_256x240.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777620_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777620_256x240.png new file mode 100644 index 00000000..9785948a Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777620_256x240.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777777_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777777_256x240.png new file mode 100644 index 00000000..323c4564 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_777777_256x240.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_cc0000_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_cc0000_256x240.png new file mode 100644 index 00000000..45ac7787 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_cc0000_256x240.png differ diff --git a/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_ffffff_256x240.png b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_ffffff_256x240.png new file mode 100644 index 00000000..fe41d2d0 Binary files /dev/null and b/src/main/webapp/resources/lib/fims/framework/datepicker/images/ui-icons_ffffff_256x240.png differ